/
descriptor.kt
212 lines (182 loc) · 6.39 KB
/
descriptor.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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package io.kotest.core.descriptors
import io.kotest.common.KotestInternal
import io.kotest.core.descriptors.Descriptor.SpecDescriptor
import io.kotest.core.descriptors.Descriptor.TestDescriptor
import io.kotest.core.names.TestName
import kotlin.reflect.KClass
typealias TestPath = io.kotest.common.TestPath
/**
* A parseable, stable, consistent identifer for a test element.
*
* The id should not depend on runtime configuration and should not change between test runs,
* unless the test, or a parent test, has been modified by the user.
*/
sealed interface Descriptor {
val id: DescriptorId
/**
* A [Descriptor] for a spec class or a script file.
*/
data class SpecDescriptor(
override val id: DescriptorId,
val kclass: KClass<*>,
) : Descriptor
/**
* A [Descriptor] for a test.
*/
data class TestDescriptor(
val parent: Descriptor,
override val id: DescriptorId,
) : Descriptor
companion object {
const val SpecDelimiter = "/"
const val TestDelimiter = " -- "
}
fun ids(): List<DescriptorId> = when (this) {
is SpecDescriptor -> listOf(this.id)
is TestDescriptor -> this.parent.ids() + this.id
}
/**
* Returns a parseable path to the test.
*
* @param includeSpec if true then the spec name is included in the path.
*/
fun path(includeSpec: Boolean = true): TestPath = when (this) {
is SpecDescriptor -> if (includeSpec) TestPath(this.id.value) else error("Cannot call path on spec with includeSpec=false")
is TestDescriptor -> when (this.parent) {
is SpecDescriptor -> when (includeSpec) {
true -> TestPath(this.parent.id.value + SpecDelimiter + this.id.value)
false -> TestPath(this.id.value)
}
is TestDescriptor -> TestPath(this.parent.path(includeSpec).value + TestDelimiter + this.id.value)
}
}
@KotestInternal
fun parts(): List<String> = when (this) {
is SpecDescriptor -> emptyList()
is TestDescriptor -> parent.parts() + listOf(this.id.value)
}
/**
* Returns true if this descriptor is for a class based test file.
*/
fun isSpec() = this is SpecDescriptor
/**
* Returns true if this descriptor is for a test case.
*/
fun isTestCase() = this is TestDescriptor
/**
* Returns true if this descriptor represents a root test case.
*/
fun isRootTest() = this is TestDescriptor && this.parent.isSpec()
/**
* Returns true if this type equals that type. For example
* if this is a spec and the rhs is also spec
*/
fun isEqualType(that: Descriptor): Boolean {
return when (this) {
is SpecDescriptor -> that.isSpec()
is TestDescriptor -> that.isTestCase()
}
}
/**
* Returns the depth of this node, where the [SpecDescriptor] has depth of 0,
* a root test has depth 1 and so on.
*/
fun depth() = parents().size - 1
/**
* Recursively returns any parent descriptors, with the spec being first in the list
* and this being last.
*/
fun parents(): List<Descriptor> = when (this) {
is SpecDescriptor -> emptyList()
is TestDescriptor -> parent.parents() + parent
}
fun chain() = parents() + this
/**
* Returns true if this descriptor is the immediate parent of the given [descriptor].
*/
fun isParentOf(descriptor: Descriptor): Boolean = when (descriptor) {
is SpecDescriptor -> false // nothing can be the parent of a spec
is TestDescriptor -> this == descriptor.parent
}
/**
* Returns true if this descriptor is ancestor (1..nth-parent) of the given [descriptor].
*/
fun isAncestorOf(descriptor: Descriptor): Boolean = when (descriptor) {
is SpecDescriptor -> false // nothing can be an ancestor of a spec
is TestDescriptor -> isParentOf(descriptor) || isAncestorOf(descriptor.parent)
}
/**
* Returns true if this descriptor is the immediate child of the given [descriptor].
*/
fun isChildOf(descriptor: Descriptor): Boolean = descriptor.isParentOf(this)
/**
* Returns true if this descriptor is a child, grandchild, etc of the given [descriptor].
*/
fun isDescendentOf(descriptor: Descriptor): Boolean = descriptor.isAncestorOf(this)
/**
* Returns true if this instance is on the path to the given description. That is, if this
* instance is either an ancestor of, of the same as, the given description.
*/
fun isOnPath(description: Descriptor): Boolean =
this == description || this.isAncestorOf(description)
/**
* Returns the prefix of the descriptor starting with the root (spec)
*/
fun getTreePrefix(): List<Descriptor> {
val ret = mutableListOf<Descriptor>()
var x = this
loop@ while (true) {
ret.add(0, x)
when (x) {
is SpecDescriptor -> {
break@loop
}
is TestDescriptor -> {
x = x.parent
}
}
}
return ret
}
/**
* Returns the [SpecDescriptor] parent for this [Descriptor].
* If this is already a spec descriptor, then returns itself.
*/
fun spec(): SpecDescriptor = when (this) {
is SpecDescriptor -> this
is TestDescriptor -> this.parent.spec()
}
}
data class DescriptorId(
val value: String,
) {
/**
* Treats the lhs and rhs both as wildcard regex one by one and check if it matches the other
*/
fun wildCardMatch(id: DescriptorId): Boolean {
val thisRegex = with(this.value) {
("\\Q$this\\E").replace("*", "\\E.*\\Q").toRegex()
}
val thatRegex = with(id.value) {
("\\Q$this\\E").replace("*", "\\E.*\\Q").toRegex()
}
return (thisRegex.matches(id.value) || thatRegex.matches(this.value))
}
}
fun SpecDescriptor.append(name: TestName): TestDescriptor =
TestDescriptor(this, DescriptorId(name.testName))
fun TestDescriptor.append(name: TestName): TestDescriptor =
this.append(name.testName)
fun Descriptor.append(name: String): TestDescriptor =
TestDescriptor(this, DescriptorId(name))
/**
* Returns the [TestDescriptor] that is the root for this [TestDescriptor].
* This may be the same descriptor that this method is invoked on, if that descriptor
* is a root test.
*/
tailrec fun TestDescriptor.root(): TestDescriptor {
return when (parent) {
is SpecDescriptor -> this // if my parent is a spec, then I am a root
is TestDescriptor -> parent.root()
}
}