Skip to content

Commit 421ae4d

Browse files
authored
Restore system environment extensions and corresponding tests. (#5070)
Fixes #5033
1 parent bf7f335 commit 421ae4d

File tree

3 files changed

+401
-0
lines changed

3 files changed

+401
-0
lines changed

kotest-extensions/api/kotest-extensions.api

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,56 @@ public final class io/kotest/extensions/system/OverrideMode$SetOrOverride : io/k
136136
public fun override (Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map;
137137
}
138138

139+
public final class io/kotest/extensions/system/SystemEnvironmentExtensionsKt {
140+
public static final fun setEnvironmentMap (Ljava/util/Map;)V
141+
public static final fun withEnvironment (Ljava/lang/String;Ljava/lang/String;Lio/kotest/extensions/system/OverrideMode;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
142+
public static final fun withEnvironment (Ljava/util/Map;Lio/kotest/extensions/system/OverrideMode;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
143+
public static final fun withEnvironment (Lkotlin/Pair;Lio/kotest/extensions/system/OverrideMode;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
144+
public static synthetic fun withEnvironment$default (Ljava/lang/String;Ljava/lang/String;Lio/kotest/extensions/system/OverrideMode;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Ljava/lang/Object;
145+
public static synthetic fun withEnvironment$default (Ljava/util/Map;Lio/kotest/extensions/system/OverrideMode;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Ljava/lang/Object;
146+
public static synthetic fun withEnvironment$default (Lkotlin/Pair;Lio/kotest/extensions/system/OverrideMode;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Ljava/lang/Object;
147+
}
148+
149+
public abstract class io/kotest/extensions/system/SystemEnvironmentListener {
150+
public fun <init> (Ljava/util/Map;Lio/kotest/extensions/system/OverrideMode;)V
151+
protected final fun changeSystemEnvironment ()V
152+
protected final fun resetSystemEnvironment ()V
153+
}
154+
155+
public final class io/kotest/extensions/system/SystemEnvironmentProjectListener : io/kotest/extensions/system/SystemEnvironmentListener, io/kotest/core/listeners/ProjectListener {
156+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/kotest/extensions/system/OverrideMode;)V
157+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/kotest/extensions/system/OverrideMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
158+
public fun <init> (Ljava/util/Map;Lio/kotest/extensions/system/OverrideMode;)V
159+
public synthetic fun <init> (Ljava/util/Map;Lio/kotest/extensions/system/OverrideMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
160+
public fun <init> (Lkotlin/Pair;Lio/kotest/extensions/system/OverrideMode;)V
161+
public synthetic fun <init> (Lkotlin/Pair;Lio/kotest/extensions/system/OverrideMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
162+
public fun afterProject (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
163+
public fun beforeProject (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
164+
}
165+
166+
public final class io/kotest/extensions/system/SystemEnvironmentTestListener : io/kotest/extensions/system/SystemEnvironmentListener, io/kotest/core/listeners/TestListener {
167+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/kotest/extensions/system/OverrideMode;)V
168+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/kotest/extensions/system/OverrideMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
169+
public fun <init> (Ljava/util/Map;Lio/kotest/extensions/system/OverrideMode;)V
170+
public synthetic fun <init> (Ljava/util/Map;Lio/kotest/extensions/system/OverrideMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
171+
public fun <init> (Lkotlin/Pair;Lio/kotest/extensions/system/OverrideMode;)V
172+
public synthetic fun <init> (Lkotlin/Pair;Lio/kotest/extensions/system/OverrideMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
173+
public fun afterAny (Lio/kotest/core/test/TestCase;Lio/kotest/engine/test/TestResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
174+
public fun afterContainer (Lio/kotest/core/test/TestCase;Lio/kotest/engine/test/TestResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
175+
public fun afterEach (Lio/kotest/core/test/TestCase;Lio/kotest/engine/test/TestResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
176+
public fun afterInvocation (Lio/kotest/core/test/TestCase;ILkotlin/coroutines/Continuation;)Ljava/lang/Object;
177+
public fun afterSpec (Lio/kotest/core/spec/Spec;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
178+
public fun afterTest (Lio/kotest/core/test/TestCase;Lio/kotest/engine/test/TestResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
179+
public fun beforeAny (Lio/kotest/core/test/TestCase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
180+
public fun beforeContainer (Lio/kotest/core/test/TestCase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
181+
public fun beforeEach (Lio/kotest/core/test/TestCase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
182+
public fun beforeInvocation (Lio/kotest/core/test/TestCase;ILkotlin/coroutines/Continuation;)Ljava/lang/Object;
183+
public fun beforeSpec (Lio/kotest/core/spec/Spec;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
184+
public fun beforeTest (Lio/kotest/core/test/TestCase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
185+
public fun finalizeSpec (Lkotlin/reflect/KClass;Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
186+
public fun prepareSpec (Lkotlin/reflect/KClass;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
187+
}
188+
139189
public final class io/kotest/extensions/system/SystemErrWireListener : io/kotest/core/listeners/TestListener {
140190
public fun <init> ()V
141191
public fun <init> (Z)V
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package io.kotest.extensions.system
2+
3+
import io.kotest.core.test.TestCase
4+
import io.kotest.core.listeners.ProjectListener
5+
import io.kotest.core.listeners.TestListener
6+
import io.kotest.engine.test.TestResult
7+
import io.kotest.extensions.system.OverrideMode.SetOrError
8+
import java.lang.reflect.Field
9+
10+
/**
11+
* Modifies System Environment with chosen key and value
12+
*
13+
* This is a helper function for code that uses Environment Variables. It changes the specific [key] from [System.getenv]
14+
* with the specified [value], only during the execution of [block].
15+
*
16+
* To do this, this function uses a trick that makes the System Environment editable, and changes [key]. Any previous
17+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
18+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
19+
*
20+
* After the execution of [block], the environment is set to what it was before.
21+
*
22+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
23+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
24+
*/
25+
inline fun <T> withEnvironment(key: String, value: String?, mode: OverrideMode = SetOrError, block: () -> T): T {
26+
return withEnvironment(key to value, mode, block)
27+
}
28+
29+
/**
30+
* Modifies System Environment with chosen key and value
31+
*
32+
* This is a helper function for code that uses Environment Variables. It changes the specific key from [System.getenv]
33+
* with the specified value, only during the execution of [block].
34+
*
35+
* To do this, this function uses a trick that makes the System Environment editable, and changes key. Any previous
36+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
37+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
38+
*
39+
* After the execution of [block], the environment is set to what it was before.
40+
*
41+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
42+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
43+
*/
44+
inline fun <T> withEnvironment(environment: Pair<String, String?>, mode: OverrideMode = SetOrError, block: () -> T): T {
45+
return withEnvironment(mapOf(environment), mode, block)
46+
}
47+
48+
/**
49+
* Modifies System Environment with chosen keys and values
50+
*
51+
* This is a helper function for code that uses Environment Variables. It changes the specific keys from [System.getenv]
52+
* with the specified values, only during the execution of [block].
53+
*
54+
* To do this, this function uses a trick that makes the System Environment editable, and changes key. Any previous
55+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
56+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
57+
*
58+
* After the execution of [block], the environment is set to what it was before.
59+
*
60+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
61+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
62+
*/
63+
inline fun <T> withEnvironment(environment: Map<String, String?>, mode: OverrideMode = SetOrError, block: () -> T): T {
64+
val isWindows = "windows" in System.getProperty("os.name").orEmpty().lowercase()
65+
val originalEnvironment = if (isWindows) {
66+
System.getenv().toSortedMap(String.CASE_INSENSITIVE_ORDER)
67+
} else {
68+
System.getenv().toMap()
69+
}
70+
71+
setEnvironmentMap(mode.override(originalEnvironment, environment))
72+
73+
try {
74+
return block()
75+
} finally {
76+
setEnvironmentMap(originalEnvironment)
77+
}
78+
}
79+
80+
@PublishedApi
81+
// Implementation inspired from https://github.com/stefanbirkner/system-rule
82+
internal fun setEnvironmentMap(map: Map<String, String?>) {
83+
val envMapOfVariables = getEditableMapOfVariables()
84+
val caseInsensitiveEnvironment = getCaseInsensitiveEnvironment()
85+
86+
envMapOfVariables.clear()
87+
caseInsensitiveEnvironment?.clear()
88+
89+
envMapOfVariables.putReplacingNulls(map)
90+
caseInsensitiveEnvironment?.putReplacingNulls(map)
91+
}
92+
93+
@Suppress("UNCHECKED_CAST")
94+
private fun getEditableMapOfVariables(): MutableMap<String, String> {
95+
val systemEnv = System.getenv()
96+
val classOfMap = systemEnv::class.java
97+
98+
return classOfMap.getDeclaredField("m").asAccessible().get(systemEnv) as MutableMap<String, String>
99+
}
100+
101+
@Suppress("UNCHECKED_CAST")
102+
private fun getCaseInsensitiveEnvironment(): MutableMap<String, String>? {
103+
val processEnvironmentClass = Class.forName("java.lang.ProcessEnvironment")
104+
105+
return try {
106+
processEnvironmentClass.getDeclaredField("theCaseInsensitiveEnvironment").asAccessible().get(null) as MutableMap<String, String>?
107+
} catch (e: NoSuchFieldException) {
108+
// Only available in Windows, ok to return null if it's not found
109+
null
110+
}
111+
}
112+
113+
private fun Field.asAccessible(): Field {
114+
return apply { isAccessible = true }
115+
}
116+
117+
118+
abstract class SystemEnvironmentListener(private val environment: Map<String, String?>,
119+
private val mode: OverrideMode) {
120+
121+
private val originalEnvironment = System.getenv().toMap()
122+
123+
protected fun changeSystemEnvironment() {
124+
setEnvironmentMap(mode.override(originalEnvironment, environment))
125+
}
126+
127+
protected fun resetSystemEnvironment() {
128+
setEnvironmentMap(originalEnvironment)
129+
}
130+
}
131+
132+
/**
133+
* Modifies System Environment with chosen keys and values
134+
*
135+
* This is a Listener for code that uses Environment Variables. It changes the specific keys from [System.getenv]
136+
* with the specified values, only during the execution of a test.
137+
*
138+
* To do this, this listener uses a trick that makes the System Environment editable, and changes the keys. Any previous
139+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
140+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
141+
*
142+
* After the execution of the test, the environment is set to what it was before.
143+
*
144+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
145+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
146+
*/
147+
class SystemEnvironmentTestListener(environment: Map<String, String?>, mode: OverrideMode = SetOrError) :
148+
SystemEnvironmentListener(environment, mode), TestListener {
149+
150+
/**
151+
* Modifies System Environment with chosen keys and values
152+
*
153+
* This is a Listener for code that uses Environment Variables. It changes the specific keys from [System.getenv]
154+
* with the specified values, only during the execution of a test.
155+
*
156+
* To do this, this listener uses a trick that makes the System Environment editable, and changes the keys. Any previous
157+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
158+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
159+
*
160+
* After the execution of the test, the environment is set to what it was before.
161+
*
162+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
163+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
164+
*/
165+
constructor(key: String, value: String?, mode: OverrideMode = SetOrError) : this(key to value, mode)
166+
167+
/**
168+
* Modifies System Environment with chosen keys and values
169+
*
170+
* This is a Listener for code that uses Environment Variables. It changes the specific keys from [System.getenv]
171+
* with the specified values, only during the execution of a test.
172+
*
173+
* To do this, this listener uses a trick that makes the System Environment editable, and changes the keys. Any previous
174+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
175+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
176+
*
177+
* After the execution of the test, the environment is set to what it was before.
178+
*
179+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
180+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
181+
*/
182+
constructor(environment: Pair<String, String?>, mode: OverrideMode = SetOrError) : this(mapOf(environment), mode)
183+
184+
override suspend fun beforeAny(testCase: TestCase) {
185+
changeSystemEnvironment()
186+
}
187+
188+
override suspend fun afterAny(testCase: TestCase, result: TestResult) {
189+
resetSystemEnvironment()
190+
}
191+
}
192+
193+
/**
194+
* Modifies System Environment with chosen keys and values
195+
*
196+
* This is a Listener for code that uses Environment Variables. It changes the specific keys from [System.getenv]
197+
* with the specified values, during the execution of the project.
198+
*
199+
* To do this, this listener uses a trick that makes the System Environment editable, and changes the keys. Any previous
200+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
201+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
202+
*
203+
* After the execution of the project, the environment is set to what it was before.
204+
*
205+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
206+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
207+
*/
208+
class SystemEnvironmentProjectListener(environment: Map<String, String?>, mode: OverrideMode = SetOrError) :
209+
SystemEnvironmentListener(environment, mode), ProjectListener {
210+
211+
212+
/**
213+
* Modifies System Environment with chosen keys and values
214+
*
215+
* This is a Listener for code that uses Environment Variables. It changes the specific keys from [System.getenv]
216+
* with the specified values, during the execution of the project.
217+
*
218+
* To do this, this listener uses a trick that makes the System Environment editable, and changes the keys. Any previous
219+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
220+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
221+
*
222+
* After the execution of the project, the environment is set to what it was before.
223+
*
224+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
225+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
226+
*/
227+
constructor(key: String, value: String?, mode: OverrideMode = SetOrError) : this(key to value, mode)
228+
229+
/**
230+
* Modifies System Environment with chosen keys and values
231+
*
232+
* This is a Listener for code that uses Environment Variables. It changes the specific keys from [System.getenv]
233+
* with the specified values, during the execution of the project.
234+
*
235+
* To do this, this listener uses a trick that makes the System Environment editable, and changes the keys. Any previous
236+
* environment (anything not overridden) will also be in the environment. If the chosen key is in the environment,
237+
* it will be changed according to [mode]. If the chosen key is not in the environment, it will be included.
238+
*
239+
* After the execution of the project, the environment is set to what it was before.
240+
*
241+
* **ATTENTION**: This code is susceptible to race conditions. If you attempt to change the environment while it was
242+
* already changed, the result is inconsistent, as the System Environment Map is a single map.
243+
*/
244+
constructor(environment: Pair<String, String?>, mode: OverrideMode = SetOrError) : this(mapOf(environment), mode)
245+
246+
override suspend fun beforeProject() {
247+
changeSystemEnvironment()
248+
}
249+
250+
override suspend fun afterProject() {
251+
resetSystemEnvironment()
252+
}
253+
}

0 commit comments

Comments
 (0)