Ktor: testing REST endpoints using Spek/KotlinTest instad of JUnit Test Class - ktor

I have a simple hello world Ktor app:
fun Application.testMe() {
intercept(ApplicationCallPipeline.Call) {
if (call.request.uri == "/")
call.respondText("Hello")
}
}
With JUnit test class I can write the test for it, as given in its documentation; as following:
class ApplicationTest {
#Test fun testRequest() = withTestApplication(Application::testMe) {
with(handleRequest(HttpMethod.Get, "/")) {
assertEquals(HttpStatusCode.OK, response.status())
assertEquals("Hello", response.content)
}
with(handleRequest(HttpMethod.Get, "/index.html")) {
assertFalse(requestHandled)
}
}
}
However, I want to do a unit test in Spek or KotlinTest, without the help of JUnit, similar to the way I do it in ScalaTest/Play; in a more declarative way:
Send a FakeRequest to the route (i.e., /) during a test.
Get the content of the page, and check for the string "hello".
The question is can I write the above test in a more declarative way in KotlinTest or Spek?

First of all, follow spek setup guide with JUnit 5
Then you can simply declare you specifications like the following
object HelloApplicationSpec: Spek({
given("an application") {
val engine = TestApplicationEngine(createTestEnvironment())
engine.start(wait = false) // for now we can't eliminate it
engine.application.main() // our main module function
with(engine) {
on("index") {
it("should return Hello World") {
handleRequest(HttpMethod.Get, "/").let { call ->
assertEquals("Hello, World!", call.response.content)
}
}
it("should return 404 on POST") {
handleRequest(HttpMethod.Post, "/", {
body = "HTTP post body"
}).let { call ->
assertFalse(call.requestHandled)
}
}
}
}
}
})
Here is my build.gradle (simplified)
buildscript {
dependencies {
classpath 'org.junit.platform:junit-platform-gradle-plugin:1.0.3'
}
repositories {
jcenter()
}
}
repositories {
jcenter()
maven { url "http://dl.bintray.com/jetbrains/spek" }
}
apply plugin: 'org.junit.platform.gradle.plugin'
junitPlatform {
filters {
engines {
include 'spek'
}
}
}
dependencies {
testCompile 'org.jetbrains.spek:spek-api:1.1.5'
testRuntime 'org.jetbrains.spek:spek-junit-platform-engine:1.1.5'
testCompile "io.ktor:ktor-server-test-host:$ktor_version"
}

Related

Cannot install ContentNegotiation in the testApplication

I am following the documentation to unit test a ktor API. In particular configuring the HttpClient for ContentNegociation with conversion of a class to JSON (https://ktor.io/docs/testing.html#configure-client)
Note the application starts and the POST endpoint works.
Here is the test code:
class DeviceInformationRouteTest {
#Test
fun testPostDeviceInformation() = testApplication {
val client = createClient {
install(ContentNegotiation) { // Error on the `install` call
json()
}
}
val deviceInformation = DeviceInformation("test")
val response = client.post("/deviceInformation") {
setBody(deviceInformation)
}
assertEquals("""{"value": "test"}""",response.bodyAsText())
assertEquals(HttpStatusCode.OK, response.status)
}
}
The problem is that the compilation fails saying:
DeviceInformationRouteTest.kt: (19, 13): 'fun <P : Pipeline<*, ApplicationCall>, B : Any, F : Any> install(plugin: Plugin<ApplicationCallPipeline, ContentNegotiationConfig, PluginInstance>, configure: ContentNegotiationConfig.() -> Unit = ...): Unit' can't be called in this context by implicit receiver. Use the explicit one if necessary
If needed, my partial build.gradle:
plugins {
application
kotlin("jvm") version "1.7.10"
id("io.ktor.plugin") version "2.1.1"
id("org.jetbrains.kotlin.plugin.serialization") version "1.7.10"
}
dependencies {
implementation("io.ktor:ktor-server-content-negotiation-jvm:2.1.1")
implementation("io.ktor:ktor-server-core-jvm:2.1.1")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:2.1.1")
implementation("io.ktor:ktor-server-netty-jvm:2.1.1")
testImplementation("io.ktor:ktor-server-tests-jvm:2.1.1")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:1.7.10")
}
I believe this is exactly as in the example, I don't know why it is failing to compile.
So it turned out I needed a different ContentNegociation plugin, from the client package. In the build.gradle.kts:
dependencies {
// ...
testImplementation("io.ktor:ktor-client-content-negotiation:2.1.1")
}
and in the test, use the correct import:
import io.ktor.client.plugins.contentnegotiation.*
// ...
#Test
fun testPostDeviceInformation() = testApplication {
val httpClient = createClient {
install(ContentNegotiation) {
json()
}
}
// ...
}
// ...

ShareStateFlow in Intelji plugin

I have a ToolWindowFactory class like this
class MyToolWindowFactory : ToolWindowFactory {
private val sharedFlow = MutableSharedFlow<String>(replay = 1)
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
CoroutineScope(Dispatchers.IO).launch {
sharedFlow.emit("VALUE")
}
CoroutineScope(Dispatchers.IO).launch {
sharedFlow.collectLatest {
println("$it")
}
}
}
}
I use SharedFlow for variables, but I get this error after run.
Exception in thread "DefaultDispatcher-worker-3" java.lang.NoClassDefFoundError: Could not initialize class kotlinx.coroutines.CoroutineExceptionHandlerImplKt
at kotlinx.coroutines.CoroutineExceptionHandlerKt.handleCoroutineException(CoroutineExceptionHandler.kt:33)
at kotlinx.coroutines.DispatchedTask.handleFatalException$kotlinx_coroutines_core(DispatchedTask.kt:95)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:64)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Does ShareStateFlow work in Intelji Plugin?
build.gradle
plugins {
id 'org.jetbrains.intellij' version '1.5.2'
id 'org.jetbrains.kotlin.jvm' version '1.6.10'
id 'java'
}
group 'org.example'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.9"
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
// See https://github.com/JetBrains/gradle-intellij-plugin/
intellij {
version = '2021.3.3'
}
patchPluginXml {
changeNotes = """
Add change notes here.<br>
<em>most HTML tags may be used</em>"""
}
test {
useJUnitPlatform()
}

How can I publish a javadoc.jar file with my Kotlin multiplatform project?

I am trying to publish my Kotlin multiplatform library to Maven Central via Sonatype. This repository requires me to include a javadoc.jar file with my artifacts. Unfortunately, the IntelliJ IDEA project wizard and the Kotlin multiplatform docs do not help me do that. When running the Gradle task dokkaJavadoc (for the official Kotlin documentation tool Dokka), I get the error "Dokka Javadoc plugin currently does not support generating documentation for multiplatform project."
I actually do not need genuine JavaDocs for publishing - an empty javadoc.jar or one with other docs generated by Dokka would suffice. Since I have been a longtime Maven user and these are my first steps with Gradle, I have no idea how to do that.
build.gradle.kts:
plugins {
kotlin("multiplatform") version "1.4.31"
id("org.jlleitschuh.gradle.ktlint") version "10.0.0"
id("io.gitlab.arturbosch.detekt") version "1.15.0"
id("org.jetbrains.dokka") version "1.4.20"
id("maven-publish")
signing
}
group = "com.marcoeckstein"
version = "0.0.3-SNAPSHOT"
publishing {
publications {
create<MavenPublication>("maven") {
pom {
val projectGitUrl = "https://github.com/marco-eckstein/kotlin-lib"
name.set(rootProject.name)
description.set(
"A general-purpose multiplatform library. " +
"Implemented in Kotlin, usable also from Java, JavaScript and more."
)
url.set(projectGitUrl)
inceptionYear.set("2021")
licenses {
license {
name.set("MIT")
url.set("https://opensource.org/licenses/MIT")
}
}
developers {
developer {
id.set("marcoeckstein.com")
name.set("Marco Eckstein")
email.set("marco.eckstein#gmx.de")
url.set("https://www.marcoeckstein.com")
}
}
issueManagement {
system.set("GitHub")
url.set("$projectGitUrl/issues")
}
scm {
connection.set("scm:git:$projectGitUrl")
developerConnection.set("scm:git:$projectGitUrl")
url.set(projectGitUrl)
}
}
}
}
repositories {
maven {
name = "sonatypeStaging"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2")
credentials(PasswordCredentials::class)
}
}
}
signing {
useGpgCmd()
sign(publishing.publications["maven"])
}
repositories {
mavenCentral()
jcenter()
}
kotlin {
targets.all {
compilations.all {
kotlinOptions {
allWarningsAsErrors = true
}
}
}
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
js(BOTH) {
browser {
testTask {
useKarma {
useChromeHeadless()
webpackConfig.cssSupport.enabled = true
}
}
}
}
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
sourceSets {
val commonMain by getting
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation("io.kotest:kotest-assertions-core:4.4.1")
}
}
val jvmMain by getting
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit5"))
implementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
val nativeMain by getting
val nativeTest by getting
}
}
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
enableExperimentalRules.set(true)
verbose.set(true)
// ktlint.disabled_rules:
// filename:
// Caught more precisely (with desired exceptions) with detekt.
// import-ordering:
// ktlint's order is not supported (yet) by IntelliJ.
// See:
// - https://github.com/pinterest/ktlint/issues/527
// - https://youtrack.jetbrains.com/issue/KT-10974
// no-wildcard-imports:
// Not desired. We want them for Java statics and Enum members.
// experimental:annotation:
// Not desired.
// experimental:multiline-if-else:
// Not desired.
disabledRules.set(
setOf(
"filename",
"import-ordering",
"no-wildcard-imports",
"experimental:annotation",
"experimental:multiline-if-else"
)
)
additionalEditorconfigFile.set(file("$projectDir/.editorconfig"))
}
detekt {
input = files("$projectDir/src/")
config = files("$projectDir/detekt-config.yml")
buildUponDefaultConfig = true
}
tasks {
withType<io.gitlab.arturbosch.detekt.Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = "11"
}
}
Also posted at Kotlin Discussions.
This answer is a cross-post from Kotlin Discussions. Credit goes to Lamba92_v2 of the JetBrains Team, who linked his solution in his project kotlingram.
I noticed I had another issue related to publishing: Signatures and POM information where not applied to all modules. But given Lamba92_v2's code I could resolve all publishing-related issues:
plugins {
kotlin("multiplatform") version "1.4.31"
id("org.jlleitschuh.gradle.ktlint") version "10.0.0"
id("io.gitlab.arturbosch.detekt") version "1.15.0"
id("org.jetbrains.dokka") version "1.4.20"
id("maven-publish")
signing
}
group = "com.marcoeckstein"
version = "0.0.3"
val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class)
val javadocJar: TaskProvider<Jar> by tasks.registering(Jar::class) {
dependsOn(dokkaHtml)
archiveClassifier.set("javadoc")
from(dokkaHtml.outputDirectory)
}
publishing {
publications.withType<MavenPublication> {
artifact(javadocJar)
pom {
val projectGitUrl = "https://github.com/marco-eckstein/kotlin-lib"
name.set(rootProject.name)
description.set(
"A general-purpose multiplatform library. " +
"Implemented in Kotlin, usable also from Java, JavaScript and more."
)
url.set(projectGitUrl)
inceptionYear.set("2021")
licenses {
license {
name.set("MIT")
url.set("https://opensource.org/licenses/MIT")
}
}
developers {
developer {
id.set("marcoeckstein.com")
name.set("Marco Eckstein")
email.set("marco.eckstein#gmx.de")
url.set("https://www.marcoeckstein.com")
}
}
issueManagement {
system.set("GitHub")
url.set("$projectGitUrl/issues")
}
scm {
connection.set("scm:git:$projectGitUrl")
developerConnection.set("scm:git:$projectGitUrl")
url.set(projectGitUrl)
}
}
the<SigningExtension>().sign(this)
}
repositories {
maven {
name = "sonatypeStaging"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2")
credentials(PasswordCredentials::class)
}
}
}
signing {
useGpgCmd()
}
repositories {
mavenCentral()
jcenter()
}
kotlin {
targets.all {
compilations.all {
kotlinOptions {
allWarningsAsErrors = true
}
}
}
jvm {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
js(BOTH) {
browser {
testTask {
useKarma {
useChromeHeadless()
webpackConfig.cssSupport.enabled = true
}
}
}
}
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
sourceSets {
val commonMain by getting
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
implementation("io.kotest:kotest-assertions-core:4.4.1")
}
}
val jvmMain by getting
val jvmTest by getting {
dependencies {
implementation(kotlin("test-junit5"))
implementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
runtimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}
}
val jsMain by getting
val jsTest by getting {
dependencies {
implementation(kotlin("test-js"))
}
}
val nativeMain by getting
val nativeTest by getting
}
}
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
enableExperimentalRules.set(true)
verbose.set(true)
// ktlint.disabled_rules:
// filename:
// Caught more precisely (with desired exceptions) with detekt.
// import-ordering:
// ktlint's order is not supported (yet) by IntelliJ.
// See:
// - https://github.com/pinterest/ktlint/issues/527
// - https://youtrack.jetbrains.com/issue/KT-10974
// no-wildcard-imports:
// Not desired. We want them for Java statics and Enum members.
// experimental:annotation:
// Not desired.
// experimental:multiline-if-else:
// Not desired.
disabledRules.set(
setOf(
"filename",
"import-ordering",
"no-wildcard-imports",
"experimental:annotation",
"experimental:multiline-if-else"
)
)
additionalEditorconfigFile.set(file("$projectDir/.editorconfig"))
}
detekt {
input = files("$projectDir/src/")
config = files("$projectDir/detekt-config.yml")
buildUponDefaultConfig = true
}
tasks {
withType<io.gitlab.arturbosch.detekt.Detekt> {
// Target version of the generated JVM bytecode. It is used for type resolution.
jvmTarget = "11"
}
}

Kotlin-multiplatform: ShadowJar gradle plugin creates empty jar

I tried to use ShadowJar gradle pluging to pack my ktor app into fat jar. But as result of shadowJar task i get every time almost empty jar. It contains only manifest (main class is properly set).
Gradle configuration (groovy):
import org.gradle.jvm.tasks.Jar
buildscript {
ext.kotlin_version = '1.3.72'
ext.ktor_version = '1.3.2'
ext.serialization_version = '0.20.0'
ext.sl4j_version = '1.6.1'
repositories { jcenter() }
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.61'
}
apply plugin: 'kotlinx-serialization'
apply plugin: 'application'
apply plugin: 'java'
mainClassName = 'com.example.JvmMainKt'
apply plugin: 'com.github.johnrengelman.shadow'
repositories {
mavenCentral()
jcenter()
}
group 'com.example'
version '0.0.1'
apply plugin: 'maven-publish'
kotlin {
jvm {
}
js {
browser {
}
nodejs {
}
}
// For ARM, should be changed to iosArm32 or iosArm64
// For Linux, should be changed to e.g. linuxX64
// For MacOS, should be changed to e.g. macosX64
// For Windows, should be changed to e.g. mingwX64
mingwX64("mingw") {
binaries {
executable {
// Change to specify fully qualified name of your application's entry point:
entryPoint = 'main'
// Specify command-line arguments, if necessary:
runTask?.args('')
}
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmMain {
dependencies {
implementation kotlin('stdlib-jdk8')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
implementation "io.ktor:ktor-serialization:$ktor_version"
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "org.slf4j:slf4j-simple:$sl4j_version"
}
}
jvmTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
}
}
jsMain {
dependencies {
implementation kotlin('stdlib-js')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
mingwMain {
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-native:$serialization_version"
}
}
mingwTest {
}
}
}
shadowJar {
mainClassName = 'com.example.JvmMainKt'
mergeServiceFiles()
}
marcu's answer is correct but he doesn't explain why that snipped solved his issue, and he's code snippet cannot be directly converted to build.gradle.kts because a cast is needed to see runtimeDependencyFiles from Kotlin. In groovy, his snippet works because groovy supports duck typing while Kotlin doesnt.
I needed this solution in Gradle Kotlin, so I'm sharing it this way.
The com.github.johnrengelman.shadow gradle plugin is designed to work with regular the java gradle plugin, which builds a single jar by default, so it automatically generates a single fat-jar based on that jar classpath.
The Kotlin-Multiplatform gradle plugin works differently, it creates jar files for each jvm target based on a lot of custom settings that is set up using a methodology that is unique to Kotlin-Multiplatform, that's why the default shadowJar task does not work out of the box on Kotlin-Multiplatform.
To solve this problem, we have to create a new ShadowJar task for each jvm target that we need a fat-jar manually, we can do it after we declare the targets (as marcu's example is showing), or during the creation of them. I'm going to show both ways, but I recommend doing during the creation to avoid having to cast objects.
Another thing is that I created function to apply the needed configuration because I have a 8 JVM targets, so this way I don't need to copy-paste the snipped 8 times, I only call the function instead.
During the creation of the target
The explanations are commented on the code:
// Add this imports on top of your build.gradle.kts file
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
// Since you need a main class, I added this default constant
// which you can change as you need:
val defaultMainClassName: String? = null
// Here I declare the function that will register the new ShadowJar task for the target
// If you need another mainClassName, you can pass it as parameter
fun KotlinJvmTarget.registerShadowJar(mainClassName: String? = defaultMainClassName) {
// We get the name from the target here to avoid conflicting
// with the name of the compilation unit
val targetName = name
// Access the main compilation
// We only want to create ShadowJar
// for the main compilation of the target, not the test
compilations.named("main") {
// Access the tasks
tasks {
// Here we register our custom ShadowJar task,
// it's being prefixed by the target name
val shadowJar = register<ShadowJar>("${targetName}ShadowJar") {
// Allows our task to be grouped alongside with other build task
// this is only for organization
group = "build"
// This is important, it adds all output of the build to the fat-jar
from(output)
// This tells ShadowJar to merge all jars in runtime environment
// into the fat-jar for this target
configurations = listOf(runtimeDependencyFiles)
// Here we configure the name of the resulting fat-jar file
// appendix makes sure we append the target name before the version
archiveAppendix.set(targetName)
// classifier is appended after the version,
// it's a common practice to set this classifier to fat-jars
archiveClassifier.set("all")
// Apply the main class name attribute
if (mainClassName != null) {
manifest {
attributes("Main-Class" to mainClassName)
}
}
// This instruction tells the ShadowJar plugin to combine
// ServiceLoader files together, this is needed because
// a lot of Kotlin libs uses service files and
// they would break without this instruction
mergeServiceFiles()
}
// Finally, we get the normal jar task for this target
// and tells kotlin to execute our recently created ShadowJar task
// after the normal jar task completes
getByName("${targetName}Jar") {
finalizedBy(shadowJar)
}
}
}
}
kotlin {
// Here we create a JVM target
jvm("jvm8") {
// We can configure anything we want
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
// If we need a ShadowJar for it,
// all we have to do now is call
// our custom function
// Here's an example of what I'm saying:
// https://stackoverflow.com/a/57951962/804976
registerShadowJar()
}
// Another one just to illustrate the example
jvm("jvm16") {
compilations.all {
kotlinOptions.jvmTarget = "16"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
registerShadowJar()
}
}
With comments removed
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
val defaultMainClassName: String? = null
fun KotlinJvmTarget.registerShadowJar(mainClassName: String? = defaultMainClassName) {
val targetName = name
compilations.named("main") {
tasks {
val shadowJar = register<ShadowJar>("${targetName}ShadowJar") {
group = "build"
from(output)
configurations = listOf(runtimeDependencyFiles)
archiveAppendix.set(targetName)
archiveClassifier.set("all")
if (mainClassName != null) {
manifest {
attributes("Main-Class" to mainClassName)
}
}
mergeServiceFiles()
}
getByName("${targetName}Jar") {
finalizedBy(shadowJar)
}
}
}
}
kotlin {
jvm("jvm8") {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
registerShadowJar()
}
jvm("jvm16") {
compilations.all {
kotlinOptions.jvmTarget = "16"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
registerShadowJar()
}
}
After the creation of the target
Now, I'm going to comment only the changes:
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
kotlin {
jvm("jvm8") {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
// Not registering on creation this time,
// we are going to register the task later
}
jvm("jvm16") {
compilations.all {
kotlinOptions.jvmTarget = "16"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
}
val defaultMainClassName: String? = null
// Instead of having KotlinJvmTarget as receiver,
// we are going to cast the target to it.
// We are also getting the target name from
// the function parameter
fun registerShadowJar(targetName: String, mainClassName: String? = defaultMainClassName) {
// Get the target casting to KotlinJvmTarget in the process
kotlin.targets.named<KotlinJvmTarget>(targetName) {
// Access the main compilation
compilations.named("main") {
tasks {
val shadowJar = register<ShadowJar>("${targetName}ShadowJar") {
group = "build"
from(output)
configurations = listOf(runtimeDependencyFiles)
archiveAppendix.set(targetName)
archiveClassifier.set("all")
if (mainClassName != null) {
manifest {
attributes("Main-Class" to mainClassName)
}
}
mergeServiceFiles()
}
getByName("${targetName}Jar") {
finalizedBy(shadowJar)
}
}
}
}
}
// Now we call the method for each JVM target that we need to create a ShadowJar
registerShadowJar("jvm8")
registerShadowJar("jvm16")
With comments removed
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
kotlin {
jvm("jvm8") {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
jvm("jvm16") {
compilations.all {
kotlinOptions.jvmTarget = "16"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
}
val defaultMainClassName: String? = null
fun registerShadowJar(targetName: String, mainClassName: String? = defaultMainClassName) {
kotlin.targets.named<KotlinJvmTarget>(targetName) {
compilations.named("main") {
tasks {
val shadowJar = register<ShadowJar>("${targetName}ShadowJar") {
group = "build"
from(output)
configurations = listOf(runtimeDependencyFiles)
archiveAppendix.set(targetName)
archiveClassifier.set("all")
if (mainClassName != null) {
manifest {
attributes("Main-Class" to mainClassName)
}
}
mergeServiceFiles()
}
getByName("${targetName}Jar") {
finalizedBy(shadowJar)
}
}
}
}
}
registerShadowJar("jvm8")
registerShadowJar("jvm16")
I've faced the same issue in the past, the syntax that was working (for groovy gradle) for me was:
shadowJar {
mergeServiceFiles()
manifest {
attributes 'Main-Class': 'com.example.JvmMainKt'
}
}
I finally found solution to my problem.
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
//...
task shadowJar2(type: ShadowJar) {
def target = kotlin.targets.jvm
from target.compilations.main.output
def runtimeClasspath = target.compilations.main.runtimeDependencyFiles
manifest {
attributes 'Main-Class': mainClassName
}
configurations = [runtimeClasspath]
}

Ktor. Can't create an instance of Httpclient in kotlin-js side

When I'm trying to obtain an instance of HttpClient on client side I have follow js-exception
List is empty.", cause_th0jdv$_0: null, name:
"NoSuchElementException", stack: "NoSuchElementException:
Unfortunately follow workaround doesn't help
js {
browser {
dceTask {
keep("ktor-ktor-io.\$\$importsForInline\$\$.ktor-ktor-io.io.ktor.utils.io")
}
}
}
SampleJs.kt
package sample
import io.ktor.client.HttpClient
import io.ktor.client.features.json.JsonFeature
import kotlin.browser.document
actual class Sample {
actual fun checkMe() = 14
}
actual object Platform {
actual val name: String = "JS"
}
#Suppress("unused")
#JsName("helloWorld")
fun helloWorld(salutation: String) {
val message = "$salutation from Kotlin.JS ${hello()}, check me value: ${Sample().checkMe()}"
document.getElementById("js-response")?.textContent = message
}
fun main() {
document.addEventListener("DOMContentLoaded", {
helloWorld("Hi!")
val client = HttpClient {
install(JsonFeature)
}
})
}
build.gradle
buildscript {
repositories {
jcenter()
}
}
plugins {
id 'org.jetbrains.kotlin.multiplatform' version '1.3.72'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.3.72'
id 'distribution'
}
repositories {
jcenter()
maven { url "https://dl.bintray.com/kotlin/ktor" }
mavenCentral()
}
def ktor_version = '1.3.2'
def logback_version = '1.2.3'
def serialization_version = '0.20.0'
kotlin {
jvm()
js {
browser {
dceTask {
keep("ktor-ktor-io.\$\$importsForInline\$\$.ktor-ktor-io.io.ktor.utils.io")
}
}
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib-common')
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serialization_version"
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmMain {
dependencies {
implementation kotlin('stdlib-jdk8')
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-server-servlet:$ktor_version"
implementation "io.ktor:ktor-html-builder:$ktor_version"
implementation "ch.qos.logback:logback-classic:$logback_version"
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version"
}
}
jvmTest {
dependencies {
implementation kotlin('test')
implementation kotlin('test-junit')
}
}
jsMain {
dependencies {
implementation npm ("text-encoding", "0.7.0")
implementation npm ("bufferutil", "4.0.1")
implementation npm ("utf-8-validate", "5.0.2")
implementation npm ("abort-controller", "3.0.0")
implementation npm ("fs", "0.0.1-security")
implementation kotlin('stdlib-js')
implementation "io.ktor:ktor-client-json-js:$ktor_version"
implementation "io.ktor:ktor-client-js:$ktor_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.8"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serialization_version"
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
}
}
}
}
jvmJar {
dependsOn(jsBrowserProductionWebpack)
from(new File(jsBrowserProductionWebpack.entry.name, jsBrowserProductionWebpack.outputPath))
}
task run(type: JavaExec, dependsOn: [jvmJar]) {
group = "application"
main = "sample.SampleJvmKt"
classpath(configurations.jvmRuntimeClasspath, jvmJar)
args = []
}
I faced with the same issue and fix it by adding
implementation("io.ktor:ktor-client-serialization:1.4.1")
In my KotlinJS proj. full list of Ktor dependencies is
implementation("io.ktor:ktor-client-js:1.4.1")
implementation("io.ktor:ktor-client-serialization:1.4.1")
P.S.: this solution has been inspired by these tickets:
Ktor Client Serialization JS: Can't obtain default serializer / NoSuchElementException
KotlinxSerializer not found in 1.2.0-rc