diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 797acea53eb..00000000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/libraries/kotlin.test/build.gradle.kts b/libraries/kotlin.test/build.gradle.kts index fc7659823e8..074e9d9a39c 100644 --- a/libraries/kotlin.test/build.gradle.kts +++ b/libraries/kotlin.test/build.gradle.kts @@ -55,6 +55,24 @@ val jsRuntimeVariant by configurations.creating { extendsFrom(jsApiVariant) } +val wasmApiVariant by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named("kotlin-api")) + attribute(KotlinPlatformType.attribute, KotlinPlatformType.wasm) + } +} +val wasmRuntimeVariant by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named("kotlin-runtime")) + attribute(KotlinPlatformType.attribute, KotlinPlatformType.wasm) + } + extendsFrom(wasmApiVariant) +} + val nativeApiVariant by configurations.creating { isCanBeConsumed = true isCanBeResolved = false @@ -76,6 +94,7 @@ val commonVariant by configurations.creating { dependencies { jvmApi(project(":kotlin-stdlib")) jsApiVariant("$group:kotlin-test-js:$version") + wasmApiVariant("$group:kotlin-test-wasm:$version") commonVariant("$group:kotlin-test-common:$version") commonVariant("$group:kotlin-test-annotations-common:$version") } @@ -114,6 +133,8 @@ val rootComponent = componentFactory.adhoc("root").apply { } addVariantsFromConfiguration(jsApiVariant) { mapToOptional() } addVariantsFromConfiguration(jsRuntimeVariant) { mapToOptional() } + addVariantsFromConfiguration(wasmApiVariant) { mapToOptional() } + addVariantsFromConfiguration(wasmRuntimeVariant) { mapToOptional() } addVariantsFromConfiguration(nativeApiVariant) { mapToOptional() } addVariantsFromConfiguration(commonVariant) { mapToOptional() } } @@ -236,6 +257,38 @@ val jsComponent = componentFactory.adhoc("js").apply { } } +val (wasmApi, wasmRuntime) = listOf("api", "runtime").map { usage -> + configurations.create("wasm${usage.capitalize()}") { + isCanBeConsumed = true + isCanBeResolved = false + attributes { + attribute(Usage.USAGE_ATTRIBUTE, objects.named("kotlin-$usage")) + attribute(KotlinPlatformType.attribute, KotlinPlatformType.js) + } + } +} +wasmRuntime.extendsFrom(wasmApi) + +dependencies { + wasmApi(project(":kotlin-stdlib-wasm")) +} + +artifacts { + val wasmJar = tasks.getByPath(":kotlin-test:kotlin-test-wasm:jsJar") + add(wasmApi.name, wasmJar) + add(wasmRuntime.name, wasmJar) +} + +val wasmComponent = componentFactory.adhoc("wasm").apply { + addVariantsFromConfiguration(wasmApi) { + mapToMavenScope("compile") + } + addVariantsFromConfiguration(wasmRuntime) { + mapToMavenScope("runtime") + } +} + + val commonMetadata by configurations.creating { isCanBeConsumed = true isCanBeResolved = false @@ -306,6 +359,12 @@ publishing { artifact(tasks.getByPath(":kotlin-test:kotlin-test-js:sourcesJar") as Jar) configureKotlinPomAttributes(project, "Kotlin Test for JS") } + create("wasm", MavenPublication::class) { + artifactId = "kotlin-test-wasm" + from(wasmComponent) + // TODO: artifact(tasks.getByPath(":kotlin-test:kotlin-test-wasm:jsSourcesJar")) + configureKotlinPomAttributes(project, "Kotlin Test for WASM") + } create("common", MavenPublication::class) { artifactId = "kotlin-test-common" from(commonMetadataComponent) diff --git a/libraries/kotlin.test/wasm/build.gradle.kts b/libraries/kotlin.test/wasm/build.gradle.kts new file mode 100644 index 00000000000..3cb45389269 --- /dev/null +++ b/libraries/kotlin.test/wasm/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +import org.jetbrains.kotlin.gradle.dsl.KotlinCompile + +plugins { + kotlin("multiplatform") +} + +val commonMainSources by task { + from( + "$rootDir/libraries/kotlin.test/common/src", + "$rootDir/libraries/kotlin.test/annotations-common/src" + ) + into("$buildDir/commonMainSources") +} + +//val commonTestSources by task { +// from("$rootDir/libraries/kotlin.test/common/src/test/kotlin") +// into("$buildDir/commonTestSources") +//} + +kotlin { + js(IR) { + nodejs() + } + + sourceSets { + val commonMain by getting { + dependencies { + api(project(":kotlin-stdlib-wasm")) + } + kotlin.srcDir(commonMainSources.get().destinationDir) + } +// val commonTest by getting { +// kotlin.srcDir(commonTestSources.get().destinationDir) +// } + val jsMain by getting { + dependencies { + api(project(":kotlin-stdlib-wasm")) + } + kotlin.srcDir("$rootDir/libraries/kotlin.test/wasm/src") + } + } +} + +tasks.withType>().configureEach { + kotlinOptions.freeCompilerArgs += listOf( + "-Xallow-kotlin-package", + "-opt-in=kotlin.ExperimentalMultiplatform", + "-opt-in=kotlin.contracts.ExperimentalContracts" + ) +} + +tasks.named("compileKotlinJs") { + (this as KotlinCompile<*>).kotlinOptions.freeCompilerArgs += "-Xir-module-name=kotlin-test" + dependsOn(commonMainSources) +} + +//tasks.named("compileTestKotlinJs") { +// dependsOn(commonTestSources) +//} diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/Annotations.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/Annotations.kt new file mode 100644 index 00000000000..cf4cc2bda6a --- /dev/null +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/Annotations.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +/** + * Marks a function as a test. + */ +@Target(AnnotationTarget.FUNCTION) +public actual annotation class Test + +/** + * Marks a test or a suite as ignored. + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) +public actual annotation class Ignore + +/** + * Marks a function to be invoked before each test. + */ +@Target(AnnotationTarget.FUNCTION) +public actual annotation class BeforeTest + +/** + * Marks a function to be invoked after each test. + */ +@Target(AnnotationTarget.FUNCTION) +public actual annotation class AfterTest diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/BareAdapter.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/BareAdapter.kt new file mode 100644 index 00000000000..0787038140d --- /dev/null +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/BareAdapter.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +/** + * A fallback adapter for the case when no framework is detected. + */ +internal open class BareAdapter : FrameworkAdapter { + + override fun suite(name: String, ignored: Boolean, suiteFn: () -> Unit) { + if (!ignored) { + suiteFn() + } + } + + override fun test(name: String, ignored: Boolean, testFn: () -> Any?) { + if (!ignored) { + testFn() + } + } +} diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/DefaultWasmAsserter.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/DefaultWasmAsserter.kt new file mode 100644 index 00000000000..19a16ab6fe9 --- /dev/null +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/DefaultWasmAsserter.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +/** + * Describes the result of an assertion execution. + */ +public interface AssertionResult { + val result: Boolean + val expected: Any? + val actual: Any? + val lazyMessage: () -> String? +} + +internal var assertHook: (AssertionResult) -> Unit = { _ -> } + +internal object DefaultWasmAsserter : Asserter { + private var e: Any? = null + private var a: Any? = null + + override fun assertEquals(message: String?, expected: Any?, actual: Any?) { + e = expected + a = actual + super.assertEquals(message, expected, actual) + } + + override fun assertNotEquals(message: String?, illegal: Any?, actual: Any?) { + e = illegal + a = actual + super.assertNotEquals(message, illegal, actual) + } + + override fun assertSame(message: String?, expected: Any?, actual: Any?) { + e = expected + a = actual + super.assertSame(message, expected, actual) + } + + override fun assertNotSame(message: String?, illegal: Any?, actual: Any?) { + e = illegal + a = actual + super.assertNotSame(message, illegal, actual) + } + + override fun assertNull(message: String?, actual: Any?) { + a = actual + super.assertNull(message, actual) + } + + override fun assertNotNull(message: String?, actual: Any?) { + a = actual + super.assertNotNull(message, actual) + } + + override fun assertTrue(lazyMessage: () -> String?, actual: Boolean) { + if (!actual) { + failWithMessage(lazyMessage, null) + } else { + invokeHook(true, lazyMessage) + } + } + + override fun assertTrue(message: String?, actual: Boolean) { + assertTrue({ message }, actual) + } + + override fun fail(message: String?): Nothing { + fail(message, null) + } + + @SinceKotlin("1.4") + override fun fail(message: String?, cause: Throwable?): Nothing { + failWithMessage({ message }, cause) + } + + private inline fun failWithMessage(lazyMessage: () -> String?, cause: Throwable?): Nothing { + val message = lazyMessage() + invokeHook(false) { message } + throw AssertionErrorWithCause(message, cause) + } + + private fun invokeHook(result: Boolean, lazyMessage: () -> String?) { + try { + assertHook(object : AssertionResult { + override val result: Boolean = result + override val expected: Any? = e + override val actual: Any? = a + override val lazyMessage: () -> String? = lazyMessage + }) + } finally { + e = null + a = null + } + } +} \ No newline at end of file diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/FrameworkAdapter.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/FrameworkAdapter.kt new file mode 100644 index 00000000000..6ed7616b493 --- /dev/null +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/FrameworkAdapter.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +/** + * Serves as a bridge to a testing framework. + * + * The tests structure is defined using internal functions suite and test, which delegate to corresponding functions of a [FrameworkAdapter]. + * Sample test layout: + * + * ``` + * suite('a suite', false, function() { + * suite('a subsuite', false, function() { + * test('a test', false, function() {...}); + * test('an ignored/pending test', true, function() {...}); + * }); + * suite('an ignored/pending test', true, function() {...}); + * }); + * ``` + * + */ +public interface FrameworkAdapter { + + /** + * Declares a test suite. + * + * @param name the name of the test suite, e.g. a class name + * @param ignored whether the test suite is ignored, e.g. marked with [Ignore] annotation + * @param suiteFn defines nested suites by calling [kotlin.test.suite] and tests by calling [kotlin.test.test] + */ + fun suite(name: String, ignored: Boolean, suiteFn: () -> Unit) + + /** + * Declares a test. + * + * @param name the test name. + * @param ignored whether the test is ignored + * @param testFn contains test body invocation + */ + fun test(name: String, ignored: Boolean, testFn: () -> Any?) +} diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TestApi.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TestApi.kt new file mode 100644 index 00000000000..c5636268ebc --- /dev/null +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TestApi.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +import kotlin.js.JsExport + +/** + * Overrides current framework adapter with a provided instance of [FrameworkAdapter]. Use in order to support custom test frameworks. + * + * If this function is not called, the test framework will be detected automatically. + * + */ +internal fun setAdapter(adapter: FrameworkAdapter) { + currentAdapter = adapter +} + +/** + * The functions below are used by the compiler to describe the tests structure, e.g. + * + * suite('a suite', false, function() { + * suite('a subsuite', false, function() { + * test('a test', false, function() {...}); + * test('an ignored/pending test', true, function() {...}); + * }); + * suite('an ignored/pending test', true, function() {...}); + * }); + */ + +internal fun suite(name: String, ignored: Boolean, suiteFn: () -> Unit) { + currentAdapter.suite(name, ignored, suiteFn) +} + +internal fun test(name: String, ignored: Boolean, testFn: () -> Any?) { + currentAdapter.test(name, ignored, testFn) +} + +internal var currentAdapter: FrameworkAdapter = BareAdapter() + +// This is called from the js-launcher alongside wasm start function +@JsExport +internal fun startUnitTests() { + // This will be filled with the corresponding code during lowering +} diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/WasmImpl.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/WasmImpl.kt new file mode 100644 index 00000000000..f6f6fddc77e --- /dev/null +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/WasmImpl.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +import kotlin.reflect.KClass + +/** + * Takes the given [block] of test code and _doesn't_ execute it. + * + * This keeps the code under test referenced, but doesn't actually test it until it is implemented. + */ +actual fun todo(block: () -> Unit) { + println("TODO at " + block) +} + +/** Platform-specific construction of AssertionError with cause */ +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun AssertionErrorWithCause(message: String?, cause: Throwable?): AssertionError = + AssertionError(message, cause) + + +@PublishedApi +internal actual fun checkResultIsFailure(exceptionClass: KClass, message: String?, blockResult: Result): T { + blockResult.fold( + onSuccess = { + asserter.fail(messagePrefix(message) + "Expected an exception of $exceptionClass to be thrown, but was completed successfully.") + }, + onFailure = { e -> + if (exceptionClass.isInstance(e)) { + @Suppress("UNCHECKED_CAST") + return e as T + } + asserter.fail(messagePrefix(message) + "Expected an exception of $exceptionClass to be thrown, but was $e", e) + } + ) +} + + +/** + * Provides the JS implementation of asserter + */ +internal actual fun lookupAsserter(): Asserter = DefaultWasmAsserter \ No newline at end of file diff --git a/libraries/stdlib/wasm/src/kotlin/Exceptions.kt b/libraries/stdlib/wasm/src/kotlin/Exceptions.kt index 9ad26c4090a..bb7d647f830 100644 --- a/libraries/stdlib/wasm/src/kotlin/Exceptions.kt +++ b/libraries/stdlib/wasm/src/kotlin/Exceptions.kt @@ -66,7 +66,7 @@ public actual open class ClassCastException actual constructor(message: String?) actual constructor() : this(null) } -public actual open class AssertionError private constructor(message: String?, cause: Throwable?) : Error(message, cause) { +public actual open class AssertionError constructor(message: String?, cause: Throwable?) : Error(message, cause) { actual constructor() : this(null) constructor(message: String?) : this(message, null) actual constructor(message: Any?) : this(message.toString(), message as? Throwable) diff --git a/settings.gradle b/settings.gradle index df59aca57ea..baa62e7f04b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -500,7 +500,8 @@ if (buildProperties.inJpsBuildIdeaSync) { ":kotlin-test", ":kotlin-test:kotlin-test-js", ":kotlin-test:kotlin-test-js-ir", - ":kotlin-test:kotlin-test-js:kotlin-test-js-it" + ":kotlin-test:kotlin-test-js:kotlin-test-js-it", + ":kotlin-test:kotlin-test-wasm" project(':kotlin-stdlib-common').projectDir = "$rootDir/libraries/stdlib/common" as File project(':kotlin-stdlib').projectDir = "$rootDir/libraries/stdlib/jvm" as File @@ -520,6 +521,7 @@ if (buildProperties.inJpsBuildIdeaSync) { project(':kotlin-test:kotlin-test-js').projectDir = "$rootDir/libraries/kotlin.test/js" as File project(':kotlin-test:kotlin-test-js-ir').projectDir = "$rootDir/libraries/kotlin.test/js-ir" as File project(':kotlin-test:kotlin-test-js:kotlin-test-js-it').projectDir = "$rootDir/libraries/kotlin.test/js/it" as File + project(':kotlin-test:kotlin-test-wasm').projectDir = "$rootDir/libraries/kotlin.test/wasm" as File } include ":compiler:android-tests"