From 8dcfc692a52b7809135cf9e8d904dbd1bcff5aca Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Wed, 12 Feb 2020 11:31:15 +0300 Subject: [PATCH 1/4] Build on JDK11 * Up the Robolectric version to support JDK11. According to https://github.com/robolectric/robolectric/issues/4085, by 4.0.2 it should support JDK11. Tests do pass after setting the version to 4.0.2, but they fail for every version released after that up to 4.3.1. It is unclear what causes this. I commit this to check how it works on the build agents, as some comments in the issue imply that on MacOS thes version, too, does not work with JDK11. * Fix fully qualified names in stacktraces in tests: - With move to JDK11, the `park` method changed its fully qualified name. * Add new sanitazing to verification of stacktraces: - Now stacktraces have additional substrings, separated by a slash: java-base/java.util.lang - They are stripped away. - Also, the placement of tabs has changed, and so the tabs are also completely removed. * Refactor `verifyStackTrace` - It used to wrap the only loop where something happened in two other loops that did nothing. Now, only the innermost loop is left. * Use a separate JavaFx dependency. * Add javafx dependency to the Swing integration * Improve error handling for JavaFX initialization - Now, the JavaFX initialization may fail with an exception in case something went wrong. - The driver for this change was that the initialization started hanging in headless environments with transition to JDK 11. - Before, the initialization logic had a flaw. If a call to one API failed, another API would be attempted. However, this approach is problematic: if the first call failed with an exception for some reason, it would leave JavaFX in a broken state where a flag would imply that the system is being initialized. Subsequent calls would then proceed to wait forever for the initialization to complete. - Now, exceptions are checked more carefully, ensuring that we only fall back to the internal API in case the public one is unavailable and not failed for some valid reason. This differentiation also allows to more boldly rethrow exceptions upwards, being more or less confident that these are relevant to the user. * Additionally test JavaFX integration with JDK8 Co-authored-by: Dmitry Khalanskiy Co-authored-by: Roman Elizarov --- README.md | 3 +- gradle.properties | 9 +++- .../test/RunningThreadStackMergeTest.kt | 8 +-- .../test/StracktraceUtils.kt | 18 +++---- .../android-unit-tests/build.gradle | 9 ++-- ui/kotlinx-coroutines-android/build.gradle | 12 ++--- ui/kotlinx-coroutines-javafx/build.gradle | 32 +++++++++++- .../src/JavaFxDispatcher.kt | 52 ++++++++++--------- ui/kotlinx-coroutines-swing/build.gradle | 12 ++++- 9 files changed, 101 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index bbd8726b42..0e47a6df46 100644 --- a/README.md +++ b/README.md @@ -203,8 +203,9 @@ to Gradle (in Preferences -> Build, Execution, Deployment -> Build Tools -> Grad ### Requirements -* JDK >= 1.8 referred to by the `JAVA_HOME` environment variable. JDK must include JavaFX. +* JDK >= 11 referred to by the `JAVA_HOME` environment variable. * JDK 1.6 referred to by the `JDK_16` environment variable. It is okay to have `JDK_16` pointing to `JAVA_HOME` for external contributions. +* JDK 1.8 referred to by the `JDK_18` environment variable. Only used by nightly stress-tests. It is okay to have `JDK_16` pointing to `JAVA_HOME` for external contributions. ## Contributions and releases diff --git a/gradle.properties b/gradle.properties index b693ededcd..3dc119ec5a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # -# Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. +# Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. # # Kotlin @@ -17,8 +17,15 @@ byte_buddy_version=1.9.3 reactor_vesion=3.2.5.RELEASE reactive_streams_version=1.0.2 rxjava2_version=2.2.8 +javafx_version=11.0.2 binary_compatibility_validator_version=0.1.1 +# Android versions +android_version=4.1.1.4 +android_support_version=26.1.0 +robolectric_version=4.0.2 +baksmali_version=2.2.7 + # JS gradle_node_version=1.2.0 node_version=8.9.3 diff --git a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt index 3f3c69cdb2..c0b7f50134 100644 --- a/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt +++ b/kotlinx-coroutines-debug/test/RunningThreadStackMergeTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.debug @@ -20,7 +20,7 @@ class RunningThreadStackMergeTest : DebugTestBase() { verifyDump( "Coroutine \"coroutine#1\":BlockingCoroutine{Active}@62230679", // <- this one is ignored "Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@50284dc4, state: RUNNING\n" + - "\tat sun.misc.Unsafe.park(Native Method)\n" + + "\tat jdk.internal.misc.Unsafe.park(Native Method)\n" + "\tat java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)\n" + "\tat java.util.concurrent.locks.AbstractQueuedSynchronizer\$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)\n" + "\tat java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:234)\n" + @@ -75,7 +75,7 @@ class RunningThreadStackMergeTest : DebugTestBase() { verifyDump( "Coroutine \"coroutine#1\":BlockingCoroutine{Active}@62230679", // <- this one is ignored "Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@3aea3c67, state: RUNNING\n" + - "\tat sun.misc.Unsafe.park(Native Method)\n" + + "\tat jdk.internal.misc.Unsafe.park(Native Method)\n" + "\tat java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)\n" + "\tat java.util.concurrent.locks.AbstractQueuedSynchronizer\$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)\n" + "\tat java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:234)\n" + @@ -116,7 +116,7 @@ class RunningThreadStackMergeTest : DebugTestBase() { verifyDump( "Coroutine \"coroutine#1\":BlockingCoroutine{Active}@62230679", // <- this one is ignored "Coroutine \"coroutine#2\":StandaloneCoroutine{Active}@3aea3c67, state: RUNNING\n" + - "\tat sun.misc.Unsafe.park(Native Method)\n" + + "\tat jdk.internal.misc.Unsafe.park(Native Method)\n" + "\tat java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)\n" + "\tat java.util.concurrent.locks.AbstractQueuedSynchronizer\$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)\n" + "\tat java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:234)\n" + diff --git a/kotlinx-coroutines-debug/test/StracktraceUtils.kt b/kotlinx-coroutines-debug/test/StracktraceUtils.kt index 2600e4a572..050bbb0192 100644 --- a/kotlinx-coroutines-debug/test/StracktraceUtils.kt +++ b/kotlinx-coroutines-debug/test/StracktraceUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.debug @@ -11,6 +11,8 @@ public fun String.trimStackTrace(): String = trimIndent() .replace(Regex(":[0-9]+"), "") .replace(Regex("#[0-9]+"), "") + .replace(Regex("(?<=\tat )[^\n]*/"), "") + .replace(Regex("\t"), "") .applyBackspace() public fun String.applyBackspace(): String { @@ -30,16 +32,12 @@ public fun String.applyBackspace(): String { public fun verifyStackTrace(e: Throwable, traces: List) { val stacktrace = toStackTrace(e) + val trimmedStackTrace = stacktrace.trimStackTrace() traces.forEach { - val expectedLines = it.trimStackTrace().split("\n") - for (i in 0 until expectedLines.size) { - traces.forEach { - assertTrue( - stacktrace.trimStackTrace().contains(it.trimStackTrace()), - "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace" - ) - } - } + assertTrue( + trimmedStackTrace.contains(it.trimStackTrace()), + "\nExpected trace element:\n$it\n\nActual stacktrace:\n$stacktrace" + ) } val causes = stacktrace.count("Caused by") diff --git a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle index 83b8dcd7d9..d724cd90d1 100644 --- a/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle +++ b/ui/kotlinx-coroutines-android/android-unit-tests/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ repositories { @@ -7,10 +7,9 @@ repositories { } dependencies { - testImplementation 'com.google.android:android:4.1.1.4' - testImplementation 'com.android.support:support-annotations:26.1.0' - testImplementation 'com.google.android:android:4.1.1.4' - testImplementation 'org.robolectric:robolectric:4.0-alpha-3' + testImplementation "com.google.android:android:$android_version" + testImplementation "com.android.support:support-annotations:$android_support_version" + testImplementation "org.robolectric:robolectric:$robolectric_version" testImplementation project(":kotlinx-coroutines-test") testImplementation project(":kotlinx-coroutines-android") } diff --git a/ui/kotlinx-coroutines-android/build.gradle b/ui/kotlinx-coroutines-android/build.gradle index b3075f780e..c05881eb6c 100644 --- a/ui/kotlinx-coroutines-android/build.gradle +++ b/ui/kotlinx-coroutines-android/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ repositories { @@ -18,12 +18,12 @@ configurations { } dependencies { - compileOnly 'com.google.android:android:4.1.1.4' - compileOnly 'com.android.support:support-annotations:26.1.0' + compileOnly "com.google.android:android:$android_version" + compileOnly "com.android.support:support-annotations:$android_support_version" - testImplementation 'com.google.android:android:4.1.1.4' - testImplementation 'org.robolectric:robolectric:4.0-alpha-3' - testImplementation 'org.smali:baksmali:2.2.7' + testImplementation "com.google.android:android:$android_version" + testImplementation "org.robolectric:robolectric:$robolectric_version" + testImplementation "org.smali:baksmali:$baksmali_version" // TODO Replace with a 1.6.x version once released to maven.google.com. r8 'com.android.tools:r8:a7ce65837bec81c62261bf0adac73d9c09d32af2' diff --git a/ui/kotlinx-coroutines-javafx/build.gradle b/ui/kotlinx-coroutines-javafx/build.gradle index 3b17101f53..25526802c8 100644 --- a/ui/kotlinx-coroutines-javafx/build.gradle +++ b/ui/kotlinx-coroutines-javafx/build.gradle @@ -1,4 +1,34 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +plugins { + id 'org.openjfx.javafxplugin' version '0.0.8' +} + +javafx { + version = javafx_version + modules = [ 'javafx.controls' ] + configuration = 'compile' +} + +task checkJdk8() { + // only fail w/o JDK_18 when actually trying to test, not during project setup phase + doLast { + if (!System.env.JDK_18) { + throw new GradleException("JDK_18 environment variable is not defined. " + + "Can't run JDK 8 compatibility tests. " + + "Please ensure JDK 8 is installed and that JDK_18 points to it.") + } + } +} + +task jdk8Test(type: Test, dependsOn: [compileTestKotlin, checkJdk8]) { + classpath = files { test.classpath } + testClassesDirs = files { test.testClassesDirs } + executable = "$System.env.JDK_18/bin/java" +} + +// Run these tests only during nightly stress test +jdk8Test.onlyIf { project.properties['stressTest'] != null } +build.dependsOn jdk8Test diff --git a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt index 4d7571c29f..e1e2d246b5 100644 --- a/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt +++ b/ui/kotlinx-coroutines-javafx/src/JavaFxDispatcher.kt @@ -11,6 +11,7 @@ import javafx.util.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.javafx.JavaFx.delay +import java.lang.UnsupportedOperationException import java.lang.reflect.* import java.util.concurrent.* import kotlin.coroutines.* @@ -115,6 +116,7 @@ private class PulseTimer : AnimationTimer() { } } +/** @return [true] if initialized successfully, and [false] if no display is detected */ internal fun initPlatform(): Boolean = PlatformInitializer.success // Lazily try to initialize JavaFx platform just once @@ -122,34 +124,34 @@ private object PlatformInitializer { val success = run { /* * Try to instantiate JavaFx platform in a way which works - * both on Java 8 and Java 11 and does not produce "illegal reflective access": - * - * 1) Try to invoke javafx.application.Platform.startup if this class is - * present in a classpath. - * 2) If it is not successful and does not because it is already started, - * fallback to PlatformImpl. - * - * Ignore exception anyway in case of unexpected changes in API, in that case - * user will have to instantiate it manually. + * both on Java 8 and Java 11 and does not produce "illegal reflective access". */ - val runnable = Runnable {} - runCatching { - // Invoke public API if it is present - Class.forName("javafx.application.Platform") - .getMethod("startup", java.lang.Runnable::class.java) - .invoke(null, runnable) - }.recoverCatching { exception -> - // Recover -> check re-initialization - val cause = exception.cause - if (exception is InvocationTargetException && cause is IllegalStateException - && "Toolkit already initialized" == cause.message) { - // Toolkit is already initialized -> success, return - Unit - } else { // Fallback to Java 8 API - Class.forName("com.sun.javafx.application.PlatformImpl") + try { + val runnable = Runnable {} + // Invoke the public API if it is present. + runCatching { + Class.forName("javafx.application.Platform") + .getMethod("startup", java.lang.Runnable::class.java) + }.map { method -> + method.invoke(null, runnable) + return@run true + } + // If we are here, it means the public API is not present. Try the private API. + Class.forName("com.sun.javafx.application.PlatformImpl") .getMethod("startup", java.lang.Runnable::class.java) .invoke(null, runnable) + true + } catch (exception: InvocationTargetException) { + // Can only happen as a result of [Method.invoke]. + val cause = exception.cause!! + when { + // Maybe the problem is that JavaFX is already initialized? Everything is good then. + cause is IllegalStateException && "Toolkit already initialized" == cause.message -> true + // If the problem is the headless environment, it is okay. + cause is UnsupportedOperationException && "Unable to open DISPLAY" == cause.message -> false + // Otherwise, the exception demonstrates an anomaly. + else -> throw cause } - }.isSuccess + } } } diff --git a/ui/kotlinx-coroutines-swing/build.gradle b/ui/kotlinx-coroutines-swing/build.gradle index 31761abe10..0f12bfb5dc 100644 --- a/ui/kotlinx-coroutines-swing/build.gradle +++ b/ui/kotlinx-coroutines-swing/build.gradle @@ -2,6 +2,16 @@ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ +plugins { + id 'org.openjfx.javafxplugin' version '0.0.8' +} + +javafx { + version = javafx_version + modules = [ 'javafx.controls' ] + configuration = 'compile' +} + dependencies { testCompile project(':kotlinx-coroutines-jdk8') -} \ No newline at end of file +} From 7abca6266641af92ba2a5da12e1c5cfcf0301ff9 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 14 Feb 2020 10:44:15 +0300 Subject: [PATCH 2/4] ~ Swing module does not need javafx --- ui/kotlinx-coroutines-swing/build.gradle | 12 +----------- ui/kotlinx-coroutines-swing/test/SwingTest.kt | 5 ++--- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/ui/kotlinx-coroutines-swing/build.gradle b/ui/kotlinx-coroutines-swing/build.gradle index 0f12bfb5dc..ad8bef0e2f 100644 --- a/ui/kotlinx-coroutines-swing/build.gradle +++ b/ui/kotlinx-coroutines-swing/build.gradle @@ -1,17 +1,7 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -plugins { - id 'org.openjfx.javafxplugin' version '0.0.8' -} - -javafx { - version = javafx_version - modules = [ 'javafx.controls' ] - configuration = 'compile' -} - dependencies { testCompile project(':kotlinx-coroutines-jdk8') } diff --git a/ui/kotlinx-coroutines-swing/test/SwingTest.kt b/ui/kotlinx-coroutines-swing/test/SwingTest.kt index 8b41b494cc..cbed5bf1e9 100644 --- a/ui/kotlinx-coroutines-swing/test/SwingTest.kt +++ b/ui/kotlinx-coroutines-swing/test/SwingTest.kt @@ -1,10 +1,9 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.swing -import javafx.application.* import kotlinx.coroutines.* import org.junit.* import org.junit.Test @@ -81,7 +80,7 @@ class SwingTest : TestBase() { join(component) } - private suspend fun join(component: SwingTest.SwingComponent) { + private suspend fun join(component: SwingComponent) { component.coroutineContext[Job]!!.join() } From 339a367d760e4db0d29c0fd23f612be39de8b3a1 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 14 Feb 2020 11:11:37 +0300 Subject: [PATCH 3/4] ~ Support build on JDK 1.8 & 11, check for publish under JDK 11 --- build.gradle | 23 +++++++++++++++++++++-- gradle.properties | 1 + ui/kotlinx-coroutines-javafx/build.gradle | 19 +++++++++++++------ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index e03179dddb..85d60cca51 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ import org.jetbrains.kotlin.konan.target.HostManager @@ -68,6 +68,7 @@ buildscript { classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version" classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomicfu_version" classpath "com.moowork.gradle:gradle-node-plugin:$gradle_node_version" + classpath "org.openjfx:javafx-plugin:$javafx_plugin_version" classpath "org.jetbrains.kotlinx:binary-compatibility-validator:$binary_compatibility_validator_version" // JMH plugins @@ -261,8 +262,26 @@ configure(subprojects.findAll { !unpublished.contains(it.name) }) { // Report Kotlin compiler version when building project println("Using Kotlin compiler version: $org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION") +// --------------- Publish only from under JDK11+ --------------- +task checkJdkForPublish { + doFirst { + String javaVersion = System.properties["java.version"] + int i = javaVersion.indexOf('.') + int javaVersionMajor = (i < 0 ? javaVersion : javaVersion.substring(0, i)).toInteger() + if (javaVersionMajor < 11) { + throw new GradleException("Project can be build for publishing only under JDK 11+, but found ${javaVersion}") + } + } +} + // --------------- Configure sub-projects that are published --------------- -task deploy(dependsOn: getTasksByName("publish", true) + getTasksByName("publishNpm", true)) +def publishTasks = getTasksByName("publish", true) + getTasksByName("publishNpm", true) + +publishTasks.each { + it.dependsOn checkJdkForPublish +} + +task deploy(dependsOn: publishTasks) apply plugin: 'base' diff --git a/gradle.properties b/gradle.properties index 3dc119ec5a..bb3e946b8e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,7 @@ reactor_vesion=3.2.5.RELEASE reactive_streams_version=1.0.2 rxjava2_version=2.2.8 javafx_version=11.0.2 +javafx_plugin_version=0.0.8 binary_compatibility_validator_version=0.1.1 # Android versions diff --git a/ui/kotlinx-coroutines-javafx/build.gradle b/ui/kotlinx-coroutines-javafx/build.gradle index 25526802c8..9d1c128239 100644 --- a/ui/kotlinx-coroutines-javafx/build.gradle +++ b/ui/kotlinx-coroutines-javafx/build.gradle @@ -2,14 +2,21 @@ * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ -plugins { - id 'org.openjfx.javafxplugin' version '0.0.8' +static int javaVersionMajor() { + String javaVersion = System.properties["java.version"] + int i = javaVersion.indexOf('.') + return (i < 0 ? javaVersion : javaVersion.substring(0, i)).toInteger() } -javafx { - version = javafx_version - modules = [ 'javafx.controls' ] - configuration = 'compile' +// JDK11+ does not bundle JavaFx and the plugin for JavaFx support is compiled with class file version 55.0 (JDK 11) +if (javaVersionMajor() >= 11) { + apply plugin: 'org.openjfx.javafxplugin' + + javafx { + version = javafx_version + modules = ['javafx.controls'] + configuration = 'compile' + } } task checkJdk8() { From ba5c161434102b2c446478820aaf0fe1667a3e3f Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Fri, 14 Feb 2020 15:13:21 +0300 Subject: [PATCH 4/4] ~ Make tests pass on JDK 1.8 & 11 --- kotlinx-coroutines-debug/test/StracktraceUtils.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/kotlinx-coroutines-debug/test/StracktraceUtils.kt b/kotlinx-coroutines-debug/test/StracktraceUtils.kt index 050bbb0192..de31ac10a4 100644 --- a/kotlinx-coroutines-debug/test/StracktraceUtils.kt +++ b/kotlinx-coroutines-debug/test/StracktraceUtils.kt @@ -13,6 +13,7 @@ public fun String.trimStackTrace(): String = .replace(Regex("#[0-9]+"), "") .replace(Regex("(?<=\tat )[^\n]*/"), "") .replace(Regex("\t"), "") + .replace("sun.misc.Unsafe.park", "jdk.internal.misc.Unsafe.park") // JDK8->JDK11 .applyBackspace() public fun String.applyBackspace(): String {