Skip to content

Commit 870938a

Browse files
authored
Merge pull request #41 from webcompere/environment-other-mocks
Add further mocking to complete the range of things affected by the mock EnvironmentVariables
2 parents 9b9e34d + b6b7300 commit 870938a

File tree

3 files changed

+242
-4
lines changed

3 files changed

+242
-4
lines changed

system-stubs-core/src/main/java/uk/org/webcompere/systemstubs/environment/EnvironmentVariableMocker.java

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22

33
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
44
import org.mockito.Mockito;
5+
import org.mockito.invocation.InvocationOnMock;
56

6-
import java.util.Map;
7-
import java.util.Stack;
7+
import java.util.*;
8+
import java.util.stream.Stream;
89

910
import static java.util.stream.Collectors.toMap;
11+
import static java.util.stream.Collectors.toSet;
1012

1113
/**
1214
* This takes control of the environment variables using {@link Mockito#mockStatic}. While there
@@ -18,14 +20,22 @@
1820
public class EnvironmentVariableMocker {
1921
private static final Stack<Map<String, String>> REPLACEMENT_ENV = new Stack<>();
2022

23+
private static final Set<String> MOCKED_METHODS = Stream.of("getenv", "environment", "toEnvironmentBlock")
24+
.collect(toSet());
25+
2126
static {
2227
try {
2328
Class<?> typeToMock = Class.forName("java.lang.ProcessEnvironment");
2429
Mockito.mockStatic(typeToMock, invocationOnMock -> {
25-
if (REPLACEMENT_ENV.empty() || !invocationOnMock.getMethod().getName().equals("getenv")) {
30+
if (REPLACEMENT_ENV.empty() || !MOCKED_METHODS.contains(invocationOnMock.getMethod().getName())) {
2631
return invocationOnMock.callRealMethod();
2732
}
28-
Map<String, String> currentMockedEnvironment = REPLACEMENT_ENV.peek();
33+
34+
if ("toEnvironmentBlock".equals(invocationOnMock.getMethod().getName())) {
35+
return simulateToEnvironmentBlock(invocationOnMock);
36+
}
37+
38+
Map<String, String> currentMockedEnvironment = getenv();
2939
if (invocationOnMock.getMethod().getParameterCount() == 0) {
3040
return filterNulls(currentMockedEnvironment);
3141
}
@@ -38,6 +48,101 @@ public class EnvironmentVariableMocker {
3848
}
3949
}
4050

51+
/**
52+
* The equivalent of <code>getenv</code> in the original ProcessEnvironment, assuming that
53+
* mocking is "turned on"
54+
* @return the current effective environment
55+
*/
56+
private static Map<String, String> getenv() {
57+
return REPLACEMENT_ENV.peek();
58+
}
59+
60+
/**
61+
* On Windows, this returns <code>String</code> and converts the inbound Map. On Linux/Mac
62+
* this takes a second parameter and returns <code>byte[]</code>. Implementations ripped
63+
* from the JDK source implementation
64+
* @param invocationOnMock the call to the mocked <code>ProcessEnvironment</code>
65+
* @return the environment serialized for the platform
66+
*/
67+
private static Object simulateToEnvironmentBlock(InvocationOnMock invocationOnMock) {
68+
if (invocationOnMock.getArguments().length == 1) {
69+
return toEnvironmentBlockWindows(invocationOnMock.getArgument(0));
70+
} else {
71+
return toEnvironmentBlockNix(invocationOnMock.getArgument(0),
72+
invocationOnMock.getArgument(1, int[].class));
73+
}
74+
}
75+
76+
/**
77+
* Ripped from the JDK implementatioin
78+
* @param m the map to convert
79+
* @return string representation
80+
*/
81+
private static String toEnvironmentBlockWindows(Map<String, String> m) {
82+
// Sort Unicode-case-insensitively by name
83+
List<Map.Entry<String,String>> list = new ArrayList<>(m.entrySet());
84+
Collections.sort(list, (e1, e2) -> NameComparator.compareNames(e1.getKey(), e2.getKey()));
85+
86+
StringBuilder sb = new StringBuilder(m.size() * 30);
87+
int cmp = -1;
88+
89+
// Some versions of MSVCRT.DLL require SystemRoot to be set.
90+
// So, we make sure that it is always set, even if not provided
91+
// by the caller.
92+
final String systemRoot = "SystemRoot";
93+
94+
for (Map.Entry<String,String> e : list) {
95+
String key = e.getKey();
96+
String value = e.getValue();
97+
if (cmp < 0 && (cmp = NameComparator.compareNames(key, systemRoot)) > 0) {
98+
// Not set, so add it here
99+
addToEnvIfSet(sb, systemRoot);
100+
}
101+
addToEnv(sb, key, value);
102+
}
103+
if (cmp < 0) {
104+
// Got to end of list and still not found
105+
addToEnvIfSet(sb, systemRoot);
106+
}
107+
if (sb.length() == 0) {
108+
// Environment was empty and SystemRoot not set in parent
109+
sb.append('\u0000');
110+
}
111+
// Block is double NUL terminated
112+
sb.append('\u0000');
113+
return sb.toString();
114+
}
115+
116+
// code taken from the original in ProcessEnvironment
117+
@SuppressFBWarnings({"PZLA_PREFER_ZERO_LENGTH_ARRAYS", "DM_DEFAULT_ENCODING"})
118+
private static byte[] toEnvironmentBlockNix(Map<String, String> m, int[] envc) {
119+
if (m == null) {
120+
return null;
121+
}
122+
int count = m.size() * 2; // For added '=' and NUL
123+
for (Map.Entry<String, String> entry : m.entrySet()) {
124+
count += entry.getKey().getBytes().length;
125+
count += entry.getValue().getBytes().length;
126+
}
127+
128+
byte[] block = new byte[count];
129+
130+
int i = 0;
131+
for (Map.Entry<String, String> entry : m.entrySet()) {
132+
final byte[] key = entry.getKey().getBytes();
133+
final byte[] value = entry.getValue().getBytes();
134+
System.arraycopy(key, 0, block, i, key.length);
135+
i += key.length;
136+
block[i++] = (byte) '=';
137+
System.arraycopy(value, 0, block, i, value.length);
138+
i += value.length + 1;
139+
// No need to write NUL byte explicitly
140+
//block[i++] = (byte) '\u0000';
141+
}
142+
envc[0] = m.size();
143+
return block;
144+
}
145+
41146
private static Map<String, String> filterNulls(Map<String, String> currentMockedEnvironment) {
42147
return currentMockedEnvironment.entrySet()
43148
.stream()
@@ -80,4 +185,48 @@ public static boolean pop() {
80185
public static boolean remove(Map<String, String> theOneToPop) {
81186
return REPLACEMENT_ENV.remove(theOneToPop);
82187
}
188+
189+
@SuppressFBWarnings("SE_COMPARATOR_SHOULD_BE_SERIALIZABLE")
190+
private static final class NameComparator
191+
implements Comparator<String> {
192+
193+
public int compare(String s1, String s2) {
194+
return compareNames(s1, s2);
195+
}
196+
197+
public static int compareNames(String s1, String s2) {
198+
// We can't use String.compareToIgnoreCase since it
199+
// canonicalizes to lower case, while Windows
200+
// canonicalizes to upper case! For example, "_" should
201+
// sort *after* "Z", not before.
202+
int n1 = s1.length();
203+
int n2 = s2.length();
204+
int min = Math.min(n1, n2);
205+
for (int i = 0; i < min; i++) {
206+
char c1 = s1.charAt(i);
207+
char c2 = s2.charAt(i);
208+
if (c1 != c2) {
209+
c1 = Character.toUpperCase(c1);
210+
c2 = Character.toUpperCase(c2);
211+
if (c1 != c2) {
212+
// No overflow because of numeric promotion
213+
return c1 - c2;
214+
}
215+
}
216+
}
217+
return n1 - n2;
218+
}
219+
}
220+
221+
// add the environment variable to the child, if it exists in parent
222+
private static void addToEnvIfSet(StringBuilder sb, String name) {
223+
String s = getenv().get(name);
224+
if (s != null) {
225+
addToEnv(sb, name, s);
226+
}
227+
}
228+
229+
private static void addToEnv(StringBuilder sb, String name, String val) {
230+
sb.append(name).append('=').append(val).append('\u0000');
231+
}
83232
}

system-stubs-core/src/test/java/uk/org/webcompere/systemstubs/environment/EnvironmentVariableMockerTest.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22

33
import org.junit.jupiter.api.AfterEach;
44
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.condition.EnabledOnOs;
56

7+
import java.io.BufferedReader;
8+
import java.io.IOException;
9+
import java.io.InputStreamReader;
10+
import java.util.Arrays;
611
import java.util.HashMap;
712
import java.util.Map;
13+
import java.util.stream.Collectors;
814

915
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.junit.jupiter.api.condition.OS.*;
1017

1118
class EnvironmentVariableMockerTest {
1219

@@ -62,4 +69,70 @@ void whenAddNullThenItIsNotInGetEnvReturn() {
6269

6370
assertThat(System.getenv()).doesNotContainKey("foo");
6471
}
72+
73+
@Test
74+
void processBuilderEnvironmentIsAffectedByMockEnvironment() {
75+
Map<String, String> newMap = new HashMap<>();
76+
newMap.put("FOO", "bar");
77+
EnvironmentVariableMocker.connect(newMap);
78+
assertThat(new ProcessBuilder().environment()).containsEntry("FOO", "bar");
79+
}
80+
81+
@EnabledOnOs({ MAC, LINUX })
82+
@Test
83+
void processBuilderEnvironmentWhenLaunchingNewApplicationIsAffected() throws Exception {
84+
Map<String, String> newMap = new HashMap<>();
85+
newMap.put("FOO", "bar");
86+
87+
EnvironmentVariableMocker.connect(newMap);
88+
89+
ProcessBuilder builder = new ProcessBuilder("/usr/bin/env");
90+
builder.environment().put("BING", "bong");
91+
String output = executeProcessAndGetOutput(builder);
92+
93+
assertThat(output).contains("FOO=bar");
94+
assertThat(output).contains("BING=bong");
95+
}
96+
97+
@EnabledOnOs({ MAC, LINUX })
98+
@Test
99+
void canLaunchWithDefaultEnvironmentAndNothingIsAdded() throws Exception {
100+
Map<String, String> newMap = new HashMap<>();
101+
102+
EnvironmentVariableMocker.connect(newMap);
103+
104+
ProcessBuilder builder = new ProcessBuilder("/usr/bin/env");
105+
String output = executeProcessAndGetOutput(builder);
106+
assertThat(output).doesNotContain("FOO=bar");
107+
assertThat(output).doesNotContain("BING=bong");
108+
}
109+
110+
@EnabledOnOs(WINDOWS)
111+
@Test
112+
void windowsProcessBuilderEnvironmentCanReadEnvironmentFromMock() throws Exception {
113+
Map<String, String> newMap = new HashMap<>();
114+
newMap.put("FOO", "bar");
115+
116+
EnvironmentVariableMocker.connect(newMap);
117+
118+
ProcessBuilder builder = new ProcessBuilder(Arrays.asList("cmd.exe", "/c", "set"));
119+
builder.environment().put("BING", "bong");
120+
String output = executeProcessAndGetOutput(builder);
121+
122+
assertThat(output).contains("FOO=bar");
123+
assertThat(output).contains("BING=bong");
124+
}
125+
126+
private String executeProcessAndGetOutput(ProcessBuilder builder) throws IOException {
127+
Process process = builder.start();
128+
129+
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
130+
return bufferedReader.lines().collect(Collectors.joining("\n"));
131+
} catch (Throwable t) {
132+
System.err.println(t.getMessage());
133+
throw t;
134+
} finally {
135+
process.destroy();
136+
}
137+
}
65138
}

system-stubs-core/src/test/java/uk/org/webcompere/systemstubs/environment/EnvironmentVariablesTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,20 @@ void setVariablesWithVarArgsSetterOddInputs() {
173173
.set("a"))
174174
.isInstanceOf(IllegalArgumentException.class);
175175
}
176+
177+
@Test
178+
void environmentVariablesWhenAccessingOtherWays() throws Exception {
179+
new EnvironmentVariables(singletonMap("FOO", "bar"))
180+
.execute(() -> {
181+
assertThat(System.getenv()).containsEntry("FOO", "bar");
182+
});
183+
}
184+
185+
@Test
186+
void environmentVariablesInProcessBuilder() throws Exception {
187+
new EnvironmentVariables(singletonMap("FOO", "bar"))
188+
.execute(() -> {
189+
assertThat(new ProcessBuilder().environment()).containsEntry("FOO", "bar");
190+
});
191+
}
176192
}

0 commit comments

Comments
 (0)