Skip to content

Commit bf7f335

Browse files
authored
Define callbacks order to include project level (#5069)
Fixes #5062 <!-- If this PR updates documentation, please update all relevant versions of the docs, see: https://github.com/kotest/kotest/tree/master/documentation/versioned_docs The documentation at https://github.com/kotest/kotest/tree/master/documentation/docs is the documentation for the next minor or major version _TO BE RELEASED_ -->
1 parent 75b5834 commit bf7f335

File tree

24 files changed

+341
-347
lines changed

24 files changed

+341
-347
lines changed

kotest-extensions/kotest-extensions-wiremock/src/jvmTest/kotlin/io/kotest/extensions/wiremock/WiremockListenerPerTestTest.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,29 +12,28 @@ import io.kotest.engine.test.TestResult
1212
import io.kotest.matchers.shouldBe
1313
import java.net.ConnectException
1414
import java.net.HttpURLConnection
15-
import java.net.URL
15+
import java.net.URI
1616

1717
@Suppress("BlockingMethodInNonBlockingContext")
1818
class WiremockListenerPerTestTest : FunSpec({
1919
val wireMockServer = WireMockServer(9000)
2020

21-
extension(WireMockListener.perTest(wireMockServer))
2221
extension(object : TestListener {
2322
override suspend fun afterTest(testCase: TestCase, result: TestResult) {
2423
shouldThrow<ConnectException> {
25-
val connection = URL("http://localhost:9000/test").openConnection() as HttpURLConnection
24+
val connection = URI.create("http://localhost:9000/test").toURL().openConnection() as HttpURLConnection
2625
connection.responseCode
2726
}
2827
}
2928
})
30-
29+
extension(WireMockListener.perTest(wireMockServer))
3130

3231
test("should have started wiremock server") {
3332
wireMockServer.stubFor(
3433
get(urlEqualTo("/test"))
3534
.willReturn(ok())
3635
)
37-
val connection = URL("http://localhost:9000/test").openConnection() as HttpURLConnection
36+
val connection = URI.create("http://localhost:9000/test").toURL().openConnection() as HttpURLConnection
3837
connection.responseCode shouldBe 200
3938
}
4039

@@ -43,7 +42,7 @@ class WiremockListenerPerTestTest : FunSpec({
4342
get(urlEqualTo("/second-test"))
4443
.willReturn(ok())
4544
)
46-
val connection = URL("http://localhost:9000/second-test").openConnection() as HttpURLConnection
45+
val connection = URI.create("http://localhost:9000/second-test").toURL().openConnection() as HttpURLConnection
4746
connection.responseCode shouldBe 200
4847
}
4948
})

kotest-framework/kotest-framework-engine/api/kotest-framework-engine.api

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,8 +1625,6 @@ public abstract class io/kotest/core/spec/Extendable {
16251625
public final fun extensions ()Ljava/util/List;
16261626
public final fun extensions (Ljava/util/List;)V
16271627
public final fun extensions ([Lio/kotest/core/extensions/Extension;)V
1628-
public final fun prependExtension (Lio/kotest/core/extensions/Extension;)V
1629-
public final fun prependExtensions (Ljava/util/List;)V
16301628
}
16311629

16321630
public final class io/kotest/core/spec/InvalidDslException : java/lang/Exception {
@@ -3245,7 +3243,6 @@ public final class io/kotest/engine/config/TestConfigResolver {
32453243
public final fun coroutineDebugProbes (Lio/kotest/core/test/TestCase;)Z
32463244
public final fun coroutineTestScope (Lio/kotest/core/test/TestCase;)Z
32473245
public final fun enabled (Lio/kotest/core/test/TestCase;)Lkotlin/jvm/functions/Function1;
3248-
public final fun extensions (Lio/kotest/core/test/TestCase;)Ljava/util/List;
32493246
public final fun failfast (Lio/kotest/core/test/TestCase;)Z
32503247
public final fun invocationTimeout-5sfh64U (Lio/kotest/core/test/TestCase;)J
32513248
public final fun invocations (Lio/kotest/core/test/TestCase;)I

kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/core/TestConfiguration.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ abstract class TestConfiguration : Extendable() {
145145
* top level callbacks.
146146
*/
147147
fun afterContainer(f: AfterContainer) {
148-
prependExtension(object : AfterContainerListener {
148+
extension(object : AfterContainerListener {
149149
override suspend fun afterContainer(testCase: TestCase, result: TestResult) {
150150
f(Tuple2(testCase, result))
151151
}
@@ -176,7 +176,7 @@ abstract class TestConfiguration : Extendable() {
176176
* top level callbacks.
177177
*/
178178
fun afterEach(f: AfterEach) {
179-
prependExtension(object : AfterEachListener {
179+
extension(object : AfterEachListener {
180180
override suspend fun afterEach(testCase: TestCase, result: TestResult) {
181181
f(Tuple2(testCase, result))
182182
}
@@ -237,7 +237,7 @@ abstract class TestConfiguration : Extendable() {
237237
* top level callbacks.
238238
*/
239239
fun afterAny(f: AfterAny) {
240-
prependExtension(object : AfterTestListener {
240+
extension(object : AfterTestListener {
241241
override suspend fun afterAny(testCase: TestCase, result: TestResult) {
242242
f(Tuple2(testCase, result))
243243
}

kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/core/spec/Extendable.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,4 @@ abstract class Extendable {
3838
require(extensions.isNotEmpty()) { "Cannot register empty list of extensions" }
3939
extensions(extensions.toList())
4040
}
41-
42-
/**
43-
* Register [Extension]s to be invoked before all other extensions that have
44-
* been directly registered on this class.
45-
*/
46-
fun prependExtensions(extensions: List<Extension>) {
47-
_extensions = extensions + _extensions
48-
}
49-
50-
/**
51-
* Registers an [Extension] to be invoked before all other extensions that have
52-
* been directly registered on this class.
53-
*/
54-
fun prependExtension(extension: Extension) {
55-
prependExtensions(listOf(extension))
56-
}
5741
}

kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/core/spec/Spec.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ abstract class Spec : TestConfiguration() {
340340
* top level callbacks.
341341
*/
342342
final override fun afterTest(f: AfterTest) {
343-
prependExtension(object : AfterTestListener {
343+
extension(object : AfterTestListener {
344344
override suspend fun afterTest(testCase: TestCase, result: TestResult) {
345345
if (testCase.spec::class == this@Spec::class)
346346
f(Tuple2(testCase, result))

kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/core/spec/style/scopes/ContainerScope.kt

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ import io.kotest.core.spec.InvalidDslException
2222
import io.kotest.core.spec.KotestTestScope
2323
import io.kotest.core.test.NestedTest
2424
import io.kotest.core.test.TestCase
25-
import io.kotest.engine.test.TestResult
2625
import io.kotest.core.test.TestScope
2726
import io.kotest.core.test.TestType
2827
import io.kotest.core.test.config.TestConfig
2928
import io.kotest.engine.config.projectConfigResolver
29+
import io.kotest.engine.test.TestResult
3030
import kotlin.coroutines.CoroutineContext
3131

3232
private val outOfOrderCallbacksException =
@@ -80,10 +80,6 @@ interface ContainerScope : TestScope {
8080
registerTest(name = name, disabled = disabled, config = config, type = TestType.Test, test = test)
8181
}
8282

83-
private fun prependExtension(extension: Extension) {
84-
testCase.spec.prependExtensions(listOf(extension))
85-
}
86-
8783
private fun appendExtension(extension: Extension) {
8884
testCase.spec.extension(extension)
8985
}
@@ -113,7 +109,7 @@ interface ContainerScope : TestScope {
113109
fun afterScope(f: suspend (TestCase) -> Unit) {
114110
val thisTestCase = this.testCase
115111
if (hasChildren()) throw outOfOrderCallbacksException
116-
prependExtension(object : TestListener {
112+
appendExtension(object : TestListener {
117113
override suspend fun afterContainer(testCase: TestCase, result: TestResult) {
118114
if (thisTestCase.descriptor == testCase.descriptor) {
119115
f(testCase)
@@ -145,7 +141,7 @@ interface ContainerScope : TestScope {
145141
fun afterAny(f: AfterAny) {
146142
if (hasChildren() && !projectConfigResolver.allowOutOfOrderCallbacks()) throw outOfOrderCallbacksException
147143
val thisTestCase = this.testCase
148-
prependExtension(object : AfterTestListener {
144+
appendExtension(object : AfterTestListener {
149145
override suspend fun afterAny(testCase: TestCase, result: TestResult) {
150146
if (thisTestCase.descriptor.isAncestorOf(testCase.descriptor)) f(Tuple2(testCase, result))
151147
}
@@ -183,7 +179,7 @@ interface ContainerScope : TestScope {
183179
fun afterContainer(f: AfterContainer) {
184180
if (hasChildren() && !projectConfigResolver.allowOutOfOrderCallbacks()) throw outOfOrderCallbacksException
185181
val thisTestCase = this.testCase
186-
prependExtension(object : AfterContainerListener {
182+
appendExtension(object : AfterContainerListener {
187183
override suspend fun afterContainer(testCase: TestCase, result: TestResult) {
188184
if (thisTestCase.descriptor.isAncestorOf(testCase.descriptor)) {
189185
f(Tuple2(testCase, result))
@@ -221,7 +217,7 @@ interface ContainerScope : TestScope {
221217
fun afterEach(f: AfterEach) {
222218
if (hasChildren() && !projectConfigResolver.allowOutOfOrderCallbacks()) throw outOfOrderCallbacksException
223219
val thisTestCase = this.testCase
224-
prependExtension(object : TestListener {
220+
appendExtension(object : TestListener {
225221
override suspend fun afterEach(testCase: TestCase, result: TestResult) {
226222
if (thisTestCase.descriptor.isAncestorOf(testCase.descriptor)) {
227223
f(Tuple2(testCase, result))

kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/config/TestConfigResolver.kt

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,13 @@ class TestConfigResolver(
172172
val projectEnabledOrReasonIf = projectConfig?.enabledOrReasonIf
173173
return { testCase ->
174174
when {
175-
disabledByTestConfig == true -> this@TestConfigResolver.disabledByTestConfig
176-
testEnabledIf != null -> if (testEnabledIf(testCase)) Enabled.Companion.enabled else disabledByEnabledIf
175+
disabledByTestConfig -> this@TestConfigResolver.disabledByTestConfig
176+
testEnabledIf != null -> if (testEnabledIf(testCase)) Enabled.enabled else disabledByEnabledIf
177177
testEnabledOrReasonIf != null -> testEnabledOrReasonIf.invoke(testCase)
178-
specEnabledIf != null -> if (specEnabledIf(testCase)) Enabled.Companion.enabled else disabledByEnabledIf
178+
specEnabledIf != null -> if (specEnabledIf(testCase)) Enabled.enabled else disabledByEnabledIf
179179
specEnabledOrReasonIf != null -> specEnabledOrReasonIf.invoke(testCase)
180180
projectEnabledOrReasonIf != null -> projectEnabledOrReasonIf.invoke(testCase)
181-
else -> Enabled.Companion.enabled
181+
else -> Enabled.enabled
182182
}
183183
}
184184
}
@@ -187,15 +187,21 @@ class TestConfigResolver(
187187
* Returns all [Extension]s applicable to the given [TestCase]. This includes extensions
188188
* included in test case config, those at the spec level, those from project config, and
189189
* globally registered extensions in the [ExtensionRegistry].
190+
*
191+
* @param order - controls the order of extensions returned
190192
*/
191-
fun extensions(testCase: TestCase): List<Extension> {
192-
return testConfigs(testCase).flatMap { it.extensions ?: emptyList() } +
193+
internal fun extensions(testCase: TestCase, order: ExtensionsOrder): List<Extension> {
194+
val ext = registry.all() +
195+
(projectConfig?.extensions ?: emptyList()) + // extensions defined at the project level
196+
loadPackageConfigs(testCase.spec).flatMap { it.extensions } + // package level extensions
193197
testCase.spec.extensions + // overriding the extensions val in the spec
194198
testCase.spec.functionOverrideCallbacks() + // spec level dsl eg override fun beforeTest(tc...) {}
195199
testCase.spec.extensions() + // added to the spec via dsl eg beforeTest { tc -> }
196-
loadPackageConfigs(testCase.spec).flatMap { it.extensions } + // package level extensions
197-
(projectConfig?.extensions ?: emptyList()) + // extensions defined at the project level
198-
registry.all()
200+
testConfigs(testCase).flatMap { it.extensions ?: emptyList() }
201+
return when (order) {
202+
ExtensionsOrder.GLOBAL_FIRST -> ext
203+
ExtensionsOrder.LOCAL_FIRST -> ext.reversed()
204+
}
199205
}
200206

201207
/**
@@ -208,3 +214,28 @@ class TestConfigResolver(
208214
return if (parent == null) config else config + testConfigs(parent)
209215
}
210216
}
217+
218+
/**
219+
* Controls the order of extensions loaded from a [TestConfigResolver].
220+
*/
221+
internal enum class ExtensionsOrder {
222+
223+
/**
224+
* Extensions are loaded with global first - eg those registered globally in the [ExtensionRegistry],
225+
* those from project config, those at the spec level, and finally those at test level.
226+
*
227+
* At each level extensions are returned in definition order. That is, if two extensions are defined
228+
* at the same level, the first one registered will be the first extension invoked.
229+
*/
230+
GLOBAL_FIRST,
231+
232+
/**
233+
* Extensions are loaded with most specific first - eg those defined in a test, those at the spec level,
234+
* those from project config, and globally registered extensions in the [ExtensionRegistry].
235+
*
236+
* At each level extensions are returned in reversed definition order. That is, if two extensions are defined
237+
* at the same level, the last one registered will be the first extension invoked.
238+
*/
239+
LOCAL_FIRST,
240+
241+
}

0 commit comments

Comments
 (0)