2
2
3
3
import edu .umd .cs .findbugs .annotations .SuppressFBWarnings ;
4
4
import org .mockito .Mockito ;
5
+ import org .mockito .invocation .InvocationOnMock ;
5
6
6
- import java .util .Map ;
7
- import java .util .Stack ;
7
+ import java .util .* ;
8
+ import java .util .stream . Stream ;
8
9
9
10
import static java .util .stream .Collectors .toMap ;
11
+ import static java .util .stream .Collectors .toSet ;
10
12
11
13
/**
12
14
* This takes control of the environment variables using {@link Mockito#mockStatic}. While there
18
20
public class EnvironmentVariableMocker {
19
21
private static final Stack <Map <String , String >> REPLACEMENT_ENV = new Stack <>();
20
22
23
+ private static final Set <String > MOCKED_METHODS = Stream .of ("getenv" , "environment" , "toEnvironmentBlock" )
24
+ .collect (toSet ());
25
+
21
26
static {
22
27
try {
23
28
Class <?> typeToMock = Class .forName ("java.lang.ProcessEnvironment" );
24
29
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 ())) {
26
31
return invocationOnMock .callRealMethod ();
27
32
}
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 ();
29
39
if (invocationOnMock .getMethod ().getParameterCount () == 0 ) {
30
40
return filterNulls (currentMockedEnvironment );
31
41
}
@@ -38,6 +48,101 @@ public class EnvironmentVariableMocker {
38
48
}
39
49
}
40
50
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
+
41
146
private static Map <String , String > filterNulls (Map <String , String > currentMockedEnvironment ) {
42
147
return currentMockedEnvironment .entrySet ()
43
148
.stream ()
@@ -80,4 +185,48 @@ public static boolean pop() {
80
185
public static boolean remove (Map <String , String > theOneToPop ) {
81
186
return REPLACEMENT_ENV .remove (theOneToPop );
82
187
}
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
+ }
83
232
}
0 commit comments