-
Notifications
You must be signed in to change notification settings - Fork 623
/
SpecExecutor.kt
144 lines (127 loc) · 6.38 KB
/
SpecExecutor.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package io.kotest.engine.spec
import io.kotest.common.ExperimentalKotest
import io.kotest.common.Platform
import io.kotest.common.flatMap
import io.kotest.common.platform
import io.kotest.core.concurrency.CoroutineDispatcherFactory
import io.kotest.core.config.ProjectConfiguration
import io.kotest.core.spec.DslDrivenSpec
import io.kotest.core.spec.Spec
import io.kotest.core.spec.SpecRef
import io.kotest.core.test.TestCase
import io.kotest.core.test.TestResult
import io.kotest.engine.interceptors.EngineContext
import io.kotest.engine.interceptors.toProjectContext
import io.kotest.engine.listener.TestEngineListener
import io.kotest.engine.spec.interceptor.ApplyExtensionsInterceptor
import io.kotest.engine.spec.interceptor.ConfigurationInContextInterceptor
import io.kotest.engine.spec.interceptor.EnabledIfSpecInterceptor
import io.kotest.engine.spec.interceptor.FinalizeSpecInterceptor
import io.kotest.engine.spec.interceptor.IgnoreNestedSpecStylesInterceptor
import io.kotest.engine.spec.interceptor.IgnoredSpecInterceptor
import io.kotest.engine.spec.interceptor.PrepareSpecInterceptor
import io.kotest.engine.spec.interceptor.ProjectContextInterceptor
import io.kotest.engine.spec.interceptor.RequiresTagSpecInterceptor
import io.kotest.engine.spec.interceptor.SpecExtensionInterceptor
import io.kotest.engine.spec.interceptor.SpecFilterInterceptor
import io.kotest.engine.spec.interceptor.SpecFinishedInterceptor
import io.kotest.engine.spec.interceptor.SpecRefExtensionInterceptor
import io.kotest.engine.spec.interceptor.SpecRefInterceptor
import io.kotest.engine.spec.interceptor.SpecStartedInterceptor
import io.kotest.engine.spec.interceptor.SystemPropertySpecFilterInterceptor
import io.kotest.engine.spec.interceptor.TagsExcludedSpecInterceptor
import io.kotest.engine.specInterceptorsForPlatform
import io.kotest.mpp.Logger
import io.kotest.mpp.bestName
import kotlin.reflect.KClass
/**
* Executes a single [SpecRef].
*
* Uses a [TestEngineListener] to notify of events in the spec lifecycle.
*
* The spec executor has two levels of interceptors:
* [io.kotest.engine.spec.interceptor.SpecRefInterceptor] are executed before the spec is created.
* [io.kotest.engine.spec.interceptor.SpecInterceptor] are executed after the spec is created.
*
*/
@ExperimentalKotest
class SpecExecutor(
private val defaultCoroutineDispatcherFactory: CoroutineDispatcherFactory,
private val context: EngineContext,
) {
private val logger = Logger(SpecExecutorDelegate::class)
private val extensions = SpecExtensions(context.configuration.registry)
private val listener = context.listener
suspend fun execute(ref: SpecRef) {
logger.log { Pair(ref.kclass.bestName(), "Received $ref") }
referenceInterceptors(ref)
}
suspend fun execute(kclass: KClass<out Spec>) {
execute(SpecRef.Reference(kclass))
}
private suspend fun referenceInterceptors(ref: SpecRef) {
val interceptors = listOfNotNull(
if (platform == Platform.JVM) EnabledIfSpecInterceptor(listener, context.configuration.registry) else null,
IgnoredSpecInterceptor(listener, context.configuration.registry),
SpecFilterInterceptor(listener, context.configuration.registry),
SystemPropertySpecFilterInterceptor(listener, context.configuration.registry),
TagsExcludedSpecInterceptor(listener, context.configuration),
if (platform == Platform.JVM) RequiresTagSpecInterceptor(listener, context.configuration, context.configuration.registry) else null,
SpecRefExtensionInterceptor(context.configuration.registry),
SpecStartedInterceptor(listener),
SpecFinishedInterceptor(listener),
if (platform == Platform.JVM) ApplyExtensionsInterceptor(context.configuration.registry) else null,
PrepareSpecInterceptor(context.configuration.registry),
FinalizeSpecInterceptor(context.configuration.registry),
)
val innerExecute: suspend (SpecRef) -> Result<Map<TestCase, TestResult>> = {
createInstance(ref).flatMap { specInterceptors(it) }
}
logger.log { Pair(ref.kclass.bestName(), "Executing ${interceptors.size} reference interceptors") }
interceptors.foldRight(innerExecute) { ext: SpecRefInterceptor, fn: suspend (SpecRef) -> Result<Map<TestCase, TestResult>> ->
{ ref -> ext.intercept(ref, fn) }
}.invoke(ref)
}
private suspend fun specInterceptors(spec: Spec): Result<Map<TestCase, TestResult>> {
val interceptors = listOfNotNull(
if (platform == Platform.JS) IgnoreNestedSpecStylesInterceptor(listener, context.configuration.registry) else null,
ProjectContextInterceptor(context.toProjectContext()),
SpecExtensionInterceptor(context.configuration.registry),
ConfigurationInContextInterceptor(context.configuration),
) + specInterceptorsForPlatform()
val initial: suspend (Spec) -> Result<Map<TestCase, TestResult>> = {
try {
val delegate = createSpecExecutorDelegate(listener, defaultCoroutineDispatcherFactory, context.configuration)
logger.log { Pair(spec::class.bestName(), "delegate=$delegate") }
Result.success(delegate.execute(spec))
} catch (t: Throwable) {
logger.log { Pair(spec::class.bestName(), "Error executing spec $t") }
Result.failure(t)
}
}
logger.log { Pair(spec::class.bestName(), "Executing ${interceptors.size} spec interceptors") }
return interceptors.foldRight(initial) { ext, fn ->
{ spec -> ext.intercept(spec, fn) }
}.invoke(spec)
}
/**
* Creates an instance of the given [SpecRef], notifies users of the instantiation event
* or instantiation failure, and returns a Result with the error or spec.
*
* After this method is called the spec is sealed.
*/
private suspend fun createInstance(ref: SpecRef): Result<Spec> =
ref.instance(context.configuration.registry)
.onFailure { extensions.specInstantiationError(ref.kclass, it) }
.flatMap { spec -> extensions.specInstantiated(spec).map { spec } }
.onSuccess { if (it is DslDrivenSpec) it.seal() }
}
interface SpecExecutorDelegate {
suspend fun execute(spec: Spec): Map<TestCase, TestResult>
}
@ExperimentalKotest
internal expect fun createSpecExecutorDelegate(
listener: TestEngineListener,
defaultCoroutineDispatcherFactory: CoroutineDispatcherFactory,
configuration: ProjectConfiguration,
): SpecExecutorDelegate