diff --git a/gradle/common/dependencies.gradle b/gradle/common/dependencies.gradle index e190ad310..d95069773 100644 --- a/gradle/common/dependencies.gradle +++ b/gradle/common/dependencies.gradle @@ -30,6 +30,9 @@ allprojects { add(mainConfigurationName, 'org.mockito:mockito-core:2.23.4') add(mainConfigurationName, 'org.mockito:mockito-inline:2.23.4') + add(mainConfigurationName, 'org.jetbrains.kotlinx:kotlinx-coroutines-core-common:1.1.1') + add(mainConfigurationName, 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1') + add(mainConfigurationName, 'org.jetbrains.kotlinx:kotlinx-coroutines-native:1.1.1') } } } diff --git a/integration-test/src/commonTest/kotlin/DefaultPackageTest.kt b/integration-test/src/commonTest/kotlin/DefaultPackageTest.kt index ff6aea826..727196664 100644 --- a/integration-test/src/commonTest/kotlin/DefaultPackageTest.kt +++ b/integration-test/src/commonTest/kotlin/DefaultPackageTest.kt @@ -2,4 +2,4 @@ import org.spekframework.spek2.Spek object DefaultPackageTest: Spek({ test("test in a default package") {} -}) \ No newline at end of file +}) diff --git a/integration-test/src/jvmTest/kotlin/org/spekframework/spek2/integration/TimeoutTest.kt b/integration-test/src/jvmTest/kotlin/org/spekframework/spek2/integration/TimeoutTest.kt new file mode 100644 index 000000000..7adfb520b --- /dev/null +++ b/integration-test/src/jvmTest/kotlin/org/spekframework/spek2/integration/TimeoutTest.kt @@ -0,0 +1,16 @@ +package org.spekframework.spek2.integration + +import org.spekframework.spek2.Spek +import org.spekframework.spek2.meta.Ignore + +// remove @Ignore to test if timeouts are working. +@Ignore +object TimeoutTest: Spek({ + test("default timeout") { + while (true) {} + } + + test("custom timeout", timeout = 2000) { + while (true) {} + } +}) diff --git a/integration-test/src/nativeTest/kotlin/Test.kt b/integration-test/src/nativeTest/kotlin/Test.kt index b4952f9a5..f73cb2aaa 100644 --- a/integration-test/src/nativeTest/kotlin/Test.kt +++ b/integration-test/src/nativeTest/kotlin/Test.kt @@ -5,6 +5,7 @@ import org.spekframework.spek2.runtime.discovery.DiscoveryContext fun main(args: Array) { val discoveryContext = DiscoveryContext.builder() + .addTest { DefaultPackageTest } .addTest { CalculatorSpecs } .addTest { EmptyGroupTest } .addTest { CalculatorSpecs } diff --git a/settings.gradle b/settings.gradle index e66add0f0..9dc45abf9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,6 +15,7 @@ pluginManagement { } enableFeaturePreview('STABLE_PUBLISHING') +enableFeaturePreview('GRADLE_METADATA') rootProject.name = 'spek' @@ -29,4 +30,4 @@ if (!hasProperty("excludeIdePlugins")) { include 'spek-ide-plugin:intellij-base-jvm' include 'spek-ide-plugin:intellij-idea' include 'spek-ide-plugin:android-studio' -} \ No newline at end of file +} diff --git a/spek-dsl/src/commonMain/kotlin/org/spekframework/spek2/dsl/dsl.kt b/spek-dsl/src/commonMain/kotlin/org/spekframework/spek2/dsl/dsl.kt index ec59d88aa..ec24249c5 100644 --- a/spek-dsl/src/commonMain/kotlin/org/spekframework/spek2/dsl/dsl.kt +++ b/spek-dsl/src/commonMain/kotlin/org/spekframework/spek2/dsl/dsl.kt @@ -42,9 +42,11 @@ interface ScopeBody { @SpekDsl interface TestContainer { + val defaultTimeout: Long + @Synonym(type = SynonymType.TEST) @Descriptions(Description(DescriptionLocation.VALUE_PARAMETER, 0)) - fun test(description: String, skip: Skip = Skip.No, body: TestBody.() -> Unit) + fun test(description: String, skip: Skip = Skip.No, timeout: Long = defaultTimeout, body: TestBody.() -> Unit) } @SpekDsl diff --git a/spek-dsl/src/commonMain/kotlin/org/spekframework/spek2/style/specification/specificationStyle.kt b/spek-dsl/src/commonMain/kotlin/org/spekframework/spek2/style/specification/specificationStyle.kt index 1f9187d81..5422eb546 100644 --- a/spek-dsl/src/commonMain/kotlin/org/spekframework/spek2/style/specification/specificationStyle.kt +++ b/spek-dsl/src/commonMain/kotlin/org/spekframework/spek2/style/specification/specificationStyle.kt @@ -36,14 +36,14 @@ class Suite(val delegate: GroupBody) : LifecycleAware by delegate { @Synonym(SynonymType.TEST) @Descriptions(Description(DescriptionLocation.VALUE_PARAMETER, 0)) - fun it(description: String, skip: Skip = Skip.No, body: TestBody.() -> Unit) { - delegate.createTest(description, skip, body) + fun it(description: String, skip: Skip = Skip.No, timeout: Long = delegate.defaultTimeout, body: TestBody.() -> Unit) { + delegate.createTest(description, skip, timeout, body) } @Synonym(SynonymType.TEST, excluded = true) @Descriptions(Description(DescriptionLocation.VALUE_PARAMETER, 0)) - fun xit(description: String, reason: String = "", body: TestBody.() -> Unit) { - delegate.createTest(description, Skip.Yes(reason), body) + fun xit(description: String, reason: String = "", timeout: Long = delegate.defaultTimeout, body: TestBody.() -> Unit) { + delegate.createTest(description, Skip.Yes(reason), timeout, body) } fun before(cb: () -> Unit) { @@ -81,6 +81,6 @@ private fun GroupBody.createSuite(description: String, skip: Skip, body: Suite.( } } -private fun GroupBody.createTest(description: String, skip: Skip, body: TestBody.() -> Unit) { - test(description, skip, body) +private fun GroupBody.createTest(description: String, skip: Skip, timeout: Long, body: TestBody.() -> Unit) { + test(description, skip, timeout, body) } diff --git a/spek-runtime/build.gradle b/spek-runtime/build.gradle index 01f227eeb..80de179ae 100644 --- a/spek-runtime/build.gradle +++ b/spek-runtime/build.gradle @@ -30,8 +30,9 @@ kotlin { sourceSets { commonMain { dependencies { - implementation kotlin('stdlib') api project(':spek-dsl') + implementation kotlin('stdlib') + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-common' } } @@ -40,6 +41,7 @@ kotlin { implementation kotlin('stdlib-jdk8') implementation kotlin('reflect') implementation 'io.github.classgraph:classgraph' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core' } } @@ -49,14 +51,23 @@ kotlin { linuxMain { dependsOn nativeMain + dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.1.1' + } } macOSMain { dependsOn nativeMain + dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.1.1' + } } windowsMain { dependsOn nativeMain + dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.1.1' + } } nativeTest { diff --git a/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/Collectors.kt b/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/Collectors.kt index 15a755b24..05e348685 100644 --- a/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/Collectors.kt +++ b/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/Collectors.kt @@ -13,7 +13,8 @@ class Collector( val root: GroupScopeImpl, private val lifecycleManager: LifecycleManager, private val fixtures: FixturesAdapter, - override val defaultCachingMode: CachingMode + override val defaultCachingMode: CachingMode, + override val defaultTimeout: Long ) : Root { private val ids = linkedMapOf() @@ -52,7 +53,7 @@ class Collector( } else { defaultCachingMode } - val collector = Collector(group, lifecycleManager, fixtures, cachingMode) + val collector = Collector(group, lifecycleManager, fixtures, cachingMode, defaultTimeout) try { body.invoke(collector) } catch (e: Throwable) { @@ -62,6 +63,7 @@ class Collector( idFor("Group Failure"), root.path.resolve("Group Failure"), root, + defaultTimeout, {}, skip, lifecycleManager @@ -71,11 +73,12 @@ class Collector( } - override fun test(description: String, skip: Skip, body: TestBody.() -> Unit) { + override fun test(description: String, skip: Skip, timeout: Long, body: TestBody.() -> Unit) { val test = TestScopeImpl( idFor(description), root.path.resolve(description), root, + defaultTimeout, body, skip, lifecycleManager diff --git a/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/Executor.kt b/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/Executor.kt index 2da04ecc3..a47c72f6b 100644 --- a/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/Executor.kt +++ b/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/Executor.kt @@ -1,5 +1,6 @@ package org.spekframework.spek2.runtime +import kotlinx.coroutines.* import org.spekframework.spek2.dsl.Skip import org.spekframework.spek2.runtime.execution.ExecutionListener import org.spekframework.spek2.runtime.execution.ExecutionRequest @@ -7,13 +8,18 @@ import org.spekframework.spek2.runtime.execution.ExecutionResult import org.spekframework.spek2.runtime.scope.GroupScopeImpl import org.spekframework.spek2.runtime.scope.ScopeImpl import org.spekframework.spek2.runtime.scope.TestScopeImpl +import kotlin.coroutines.CoroutineContext -class Executor { +class Executor: CoroutineScope { + private val job = Job() + override val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job fun execute(request: ExecutionRequest) { request.executionListener.executionStart() request.roots.forEach { execute(it, request.executionListener) } request.executionListener.executionFinish() + job.cancel() } private fun execute(scope: ScopeImpl, listener: ExecutionListener) { @@ -24,11 +30,26 @@ class Executor { val result = executeSafely { try { - scope.before() - scope.execute() + when (scope) { + is GroupScopeImpl -> { + scope.before() + scope.getChildren().forEach { execute(it, listener) } + } + is TestScopeImpl -> { + doRunBlocking { + // this needs to be here, in K/N the event loop + // is started during a runBlocking call. Calling + // any builders outside that will throw an exception. + val job = this@Executor.launch { + scope.before() + scope.execute() + } - if (scope is GroupScopeImpl) { - scope.getChildren().forEach { execute(it, listener) } + withTimeout(scope.timeout) { + job.join() + } + } + } } } finally { scope.after() @@ -64,3 +85,5 @@ class Executor { is TestScopeImpl -> listener.testIgnored(scope, reason) } } + +expect fun doRunBlocking(block: suspend CoroutineScope.() -> Unit) diff --git a/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/SpekRuntime.kt b/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/SpekRuntime.kt index 9f982124f..6ca57967d 100644 --- a/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/SpekRuntime.kt +++ b/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/SpekRuntime.kt @@ -10,6 +10,7 @@ import org.spekframework.spek2.runtime.lifecycle.LifecycleManager import org.spekframework.spek2.runtime.scope.* import org.spekframework.spek2.runtime.util.ClassUtil +private const val DEFAULT_TIMEOUT = 10000L class SpekRuntime { fun discover(discoveryRequest: DiscoveryRequest): DiscoveryResult { val scopes = discoveryRequest.context.getTests() @@ -46,7 +47,7 @@ class SpekRuntime { className } val classScope = GroupScopeImpl(ScopeId(ScopeType.Class, qualifiedName), path, null, Skip.No, lifecycleManager, false) - val collector = Collector(classScope, lifecycleManager, fixtures, CachingMode.TEST) + val collector = Collector(classScope, lifecycleManager, fixtures, CachingMode.TEST, DEFAULT_TIMEOUT) try { instance.root.invoke(collector) @@ -56,6 +57,7 @@ class SpekRuntime { ScopeId(ScopeType.Scope, "Discovery failure"), path.resolve("Discovery failure"), classScope, + DEFAULT_TIMEOUT, {}, Skip.No, lifecycleManager diff --git a/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/scope/Scopes.kt b/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/scope/Scopes.kt index e0bf7f759..69fa2a022 100644 --- a/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/scope/Scopes.kt +++ b/spek-runtime/src/commonMain/kotlin/org/spekframework/spek2/runtime/scope/Scopes.kt @@ -81,6 +81,7 @@ class TestScopeImpl( id: ScopeId, path: Path, override val parent: GroupScope, + val timeout: Long, private val body: TestBody.() -> Unit, skip: Skip, lifecycleManager: LifecycleManager diff --git a/spek-runtime/src/jvmMain/kotlin/org/spekframework/spek2/runtime/executors.kt b/spek-runtime/src/jvmMain/kotlin/org/spekframework/spek2/runtime/executors.kt new file mode 100644 index 000000000..16d989f4b --- /dev/null +++ b/spek-runtime/src/jvmMain/kotlin/org/spekframework/spek2/runtime/executors.kt @@ -0,0 +1,10 @@ +package org.spekframework.spek2.runtime + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking + +actual fun doRunBlocking(block: suspend CoroutineScope.() -> Unit) { + runBlocking { + block() + } +} diff --git a/spek-runtime/src/nativeMain/kotlin/org/spekframework/spek2/runtime/executors.kt b/spek-runtime/src/nativeMain/kotlin/org/spekframework/spek2/runtime/executors.kt new file mode 100644 index 000000000..16d989f4b --- /dev/null +++ b/spek-runtime/src/nativeMain/kotlin/org/spekframework/spek2/runtime/executors.kt @@ -0,0 +1,10 @@ +package org.spekframework.spek2.runtime + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking + +actual fun doRunBlocking(block: suspend CoroutineScope.() -> Unit) { + runBlocking { + block() + } +}