1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.surefire.booter;
20
21 import javax.annotation.Nonnull;
22
23 import java.io.ByteArrayOutputStream;
24 import java.io.IOException;
25 import java.net.InetSocketAddress;
26 import java.net.MalformedURLException;
27 import java.net.URI;
28 import java.nio.Buffer;
29 import java.nio.ByteBuffer;
30 import java.nio.channels.ServerSocketChannel;
31 import java.nio.channels.SocketChannel;
32 import java.util.UUID;
33 import java.util.concurrent.Callable;
34 import java.util.concurrent.FutureTask;
35
36 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
37 import org.apache.maven.surefire.api.booter.MasterProcessChannelDecoder;
38 import org.apache.maven.surefire.api.booter.MasterProcessChannelEncoder;
39 import org.apache.maven.surefire.api.fork.ForkNodeArguments;
40 import org.apache.maven.surefire.api.util.internal.WritableBufferedByteChannel;
41 import org.apache.maven.surefire.booter.spi.AbstractMasterProcessChannelProcessorFactory;
42 import org.apache.maven.surefire.booter.spi.CommandChannelDecoder;
43 import org.apache.maven.surefire.booter.spi.EventChannelEncoder;
44 import org.apache.maven.surefire.booter.spi.LegacyMasterProcessChannelProcessorFactory;
45 import org.apache.maven.surefire.booter.spi.SurefireMasterProcessChannelProcessorFactory;
46 import org.apache.maven.surefire.shared.utils.cli.ShutdownHookUtils;
47 import org.apache.maven.surefire.spi.MasterProcessChannelProcessorFactory;
48 import org.junit.Rule;
49 import org.junit.Test;
50 import org.junit.function.ThrowingRunnable;
51 import org.junit.rules.ErrorCollector;
52 import org.junit.runner.RunWith;
53 import org.mockito.ArgumentCaptor;
54 import org.mockito.Captor;
55 import org.mockito.Mock;
56 import org.mockito.invocation.InvocationOnMock;
57 import org.mockito.stubbing.Answer;
58 import org.powermock.core.classloader.annotations.PowerMockIgnore;
59 import org.powermock.core.classloader.annotations.PrepareForTest;
60 import org.powermock.modules.junit4.PowerMockRunner;
61
62 import static java.nio.charset.StandardCharsets.US_ASCII;
63 import static java.util.concurrent.TimeUnit.SECONDS;
64 import static org.apache.maven.surefire.api.util.internal.Channels.newBufferedChannel;
65 import static org.assertj.core.api.Assertions.assertThat;
66 import static org.assertj.core.api.Assertions.fail;
67 import static org.mockito.ArgumentMatchers.any;
68 import static org.mockito.ArgumentMatchers.anyString;
69 import static org.mockito.ArgumentMatchers.eq;
70 import static org.mockito.ArgumentMatchers.same;
71 import static org.mockito.Mockito.mock;
72 import static org.mockito.Mockito.times;
73 import static org.mockito.Mockito.verify;
74 import static org.mockito.Mockito.verifyZeroInteractions;
75 import static org.powermock.api.mockito.PowerMockito.doAnswer;
76 import static org.powermock.api.mockito.PowerMockito.doCallRealMethod;
77 import static org.powermock.api.mockito.PowerMockito.doNothing;
78 import static org.powermock.api.mockito.PowerMockito.doThrow;
79 import static org.powermock.api.mockito.PowerMockito.mockStatic;
80 import static org.powermock.api.mockito.PowerMockito.verifyNoMoreInteractions;
81 import static org.powermock.api.mockito.PowerMockito.verifyPrivate;
82 import static org.powermock.api.mockito.PowerMockito.when;
83 import static org.powermock.reflect.Whitebox.invokeMethod;
84 import static org.powermock.reflect.Whitebox.setInternalState;
85
86
87
88
89 @RunWith(PowerMockRunner.class)
90 @PrepareForTest({PpidChecker.class, ForkedBooter.class, EventChannelEncoder.class, ShutdownHookUtils.class})
91 @PowerMockIgnore({"org.jacoco.agent.rt.*", "com.vladium.emma.rt.*"})
92 public class ForkedBooterMockTest {
93 @Rule
94 public final ErrorCollector errorCollector = new ErrorCollector();
95
96 @Mock
97 private PpidChecker pluginProcessChecker;
98
99 @Mock
100 private ForkedBooter booter;
101
102 @Mock
103 private MasterProcessChannelProcessorFactory channelProcessorFactory;
104
105 @Mock
106 private ConsoleLogger logger;
107
108 @Captor
109 private ArgumentCaptor<String[]> capturedArgs;
110
111 @Captor
112 private ArgumentCaptor<ForkedBooter> capturedBooter;
113
114 @Test
115 public void shouldCheckNewPingMechanism() throws Exception {
116 boolean canUse = invokeMethod(ForkedBooter.class, "canUseNewPingMechanism", (PpidChecker) null);
117 assertThat(canUse).isFalse();
118
119 when(pluginProcessChecker.canUse()).thenReturn(false);
120 canUse = invokeMethod(ForkedBooter.class, "canUseNewPingMechanism", pluginProcessChecker);
121 assertThat(canUse).isFalse();
122
123 when(pluginProcessChecker.canUse()).thenReturn(true);
124 canUse = invokeMethod(ForkedBooter.class, "canUseNewPingMechanism", pluginProcessChecker);
125 assertThat(canUse).isTrue();
126 }
127
128 @Test
129 public void testMain() throws Exception {
130 mockStatic(ForkedBooter.class);
131
132 doCallRealMethod().when(ForkedBooter.class, "run", capturedBooter.capture(), capturedArgs.capture());
133
134 String[] args = new String[] {"/", "dump", "surefire.properties", "surefire-effective.properties"};
135 invokeMethod(ForkedBooter.class, "run", booter, args);
136
137 assertThat(capturedBooter.getAllValues()).hasSize(1).contains(booter);
138
139 assertThat(capturedArgs.getAllValues()).hasSize(1);
140 assertThat(capturedArgs.getAllValues().get(0)[0]).isEqualTo("/");
141 assertThat(capturedArgs.getAllValues().get(0)[1]).isEqualTo("dump");
142 assertThat(capturedArgs.getAllValues().get(0)[2]).isEqualTo("surefire.properties");
143 assertThat(capturedArgs.getAllValues().get(0)[3]).isEqualTo("surefire-effective.properties");
144
145 verifyPrivate(booter, times(1))
146 .invoke("setupBooter", same(args[0]), same(args[1]), same(args[2]), same(args[3]));
147
148 verifyPrivate(booter, times(1)).invoke("execute");
149
150 verifyNoMoreInteractions(booter);
151 }
152
153 @Test
154 public void testMainWithError() throws Exception {
155 mockStatic(ForkedBooter.class);
156
157 doCallRealMethod().when(ForkedBooter.class, "run", any(ForkedBooter.class), any(String[].class));
158
159 doThrow(new RuntimeException("dummy exception")).when(booter, "execute");
160
161 doNothing()
162 .when(
163 booter,
164 "setupBooter",
165 any(String.class),
166 any(String.class),
167 any(String.class),
168 any(String.class));
169
170 setInternalState(booter, "logger", logger);
171
172 String[] args = new String[] {"/", "dump", "surefire.properties", "surefire-effective.properties"};
173 invokeMethod(ForkedBooter.class, "run", booter, args);
174
175 verifyPrivate(booter, times(1))
176 .invoke("setupBooter", same(args[0]), same(args[1]), same(args[2]), same(args[3]));
177
178 verifyPrivate(booter, times(1)).invoke("execute");
179
180 verify(logger, times(1)).error(eq("dummy exception"), any(RuntimeException.class));
181
182 verifyPrivate(booter, times(1)).invoke("cancelPingScheduler");
183
184 verifyPrivate(booter, times(1)).invoke("exit1");
185
186 verifyNoMoreInteractions(booter);
187 }
188
189 @Test
190 public void shouldNotCloseChannelProcessorFactory() throws Exception {
191 setInternalState(booter, "channelProcessorFactory", (MasterProcessChannelProcessorFactory) null);
192
193 doCallRealMethod().when(booter, "closeForkChannel");
194
195 invokeMethod(booter, "closeForkChannel");
196
197 verifyZeroInteractions(channelProcessorFactory);
198 }
199
200 @Test
201 public void shouldCloseChannelProcessorFactory() throws Exception {
202 setInternalState(booter, "channelProcessorFactory", channelProcessorFactory);
203
204 doCallRealMethod().when(booter, "closeForkChannel");
205
206 invokeMethod(booter, "closeForkChannel");
207
208 verify(channelProcessorFactory, times(1)).close();
209 verifyNoMoreInteractions(channelProcessorFactory);
210 }
211
212 @Test
213 public void shouldFailOnCloseChannelProcessorFactory() throws Exception {
214 setInternalState(booter, "channelProcessorFactory", channelProcessorFactory);
215
216 doThrow(new IOException()).when(channelProcessorFactory).close();
217
218 doCallRealMethod().when(booter, "closeForkChannel");
219
220 invokeMethod(booter, "closeForkChannel");
221
222 verify(channelProcessorFactory, times(1)).close();
223 verifyNoMoreInteractions(channelProcessorFactory);
224 }
225
226 @Test
227 public void shouldLookupLegacyDecoderFactory() throws Exception {
228 mockStatic(ForkedBooter.class);
229
230 doCallRealMethod().when(ForkedBooter.class, "lookupDecoderFactory", anyString());
231
232 try (MasterProcessChannelProcessorFactory factory =
233 invokeMethod(ForkedBooter.class, "lookupDecoderFactory", "pipe://3")) {
234 assertThat(factory).isInstanceOf(LegacyMasterProcessChannelProcessorFactory.class);
235
236 assertThat(factory.canUse("pipe://3")).isTrue();
237
238 assertThat(factory.canUse("-- whatever --")).isFalse();
239
240 errorCollector.checkThrows(MalformedURLException.class, new ThrowingRunnable() {
241 @Override
242 public void run() throws Throwable {
243 factory.connect("tcp://localhost:123");
244 fail("should not connect to the port 123");
245 }
246 });
247
248 factory.connect("pipe://3");
249
250 ForkNodeArguments args = new ForkedNodeArg(1, false);
251 MasterProcessChannelDecoder decoder = factory.createDecoder(args);
252 assertThat(decoder).isInstanceOf(CommandChannelDecoder.class);
253 MasterProcessChannelEncoder encoder = factory.createEncoder(args);
254 assertThat(encoder).isInstanceOf(EventChannelEncoder.class);
255 }
256 }
257
258 @Test
259 @SuppressWarnings("checkstyle:magicnumber")
260 public void shouldScheduleFlushes() throws Exception {
261 final ByteArrayOutputStream out = new ByteArrayOutputStream();
262 class Factory extends AbstractMasterProcessChannelProcessorFactory {
263 @Override
264 public boolean canUse(String channelConfig) {
265 return false;
266 }
267
268 @Override
269 public void connect(String channelConfig) {}
270
271 @Override
272 public MasterProcessChannelDecoder createDecoder(@Nonnull ForkNodeArguments args) {
273 return null;
274 }
275
276 @Override
277 public MasterProcessChannelEncoder createEncoder(@Nonnull ForkNodeArguments args) {
278 return null;
279 }
280
281 public void runScheduler() throws InterruptedException {
282 final WritableBufferedByteChannel channel = newBufferedChannel(out);
283 schedulePeriodicFlusher(100, channel);
284 Thread t = new Thread() {
285 @Override
286 public void run() {
287 for (int i = 0; i < 10; i++) {
288 try {
289 channel.write(ByteBuffer.wrap(new byte[] {1}));
290 Thread.sleep(75);
291 } catch (Exception e) {
292
293 }
294 }
295 }
296 };
297 t.setDaemon(true);
298 t.start();
299 t.join(5000L);
300 }
301 }
302
303 Factory factory = new Factory();
304 factory.runScheduler();
305 factory.close();
306 assertThat(out.size()).isPositive();
307 assertThat(out.size()).isLessThanOrEqualTo(10);
308 }
309
310 @Test
311 public void shouldLookupSurefireDecoderFactory() throws Exception {
312 mockStatic(ForkedBooter.class);
313
314 doCallRealMethod().when(ForkedBooter.class, "lookupDecoderFactory", anyString());
315
316 try (ServerSocketChannel server = ServerSocketChannel.open()) {
317 server.bind(new InetSocketAddress(0));
318 int serverPort = ((InetSocketAddress) server.getLocalAddress()).getPort();
319
320 try (MasterProcessChannelProcessorFactory factory =
321 invokeMethod(ForkedBooter.class, "lookupDecoderFactory", "tcp://localhost:" + serverPort)) {
322 assertThat(factory).isInstanceOf(SurefireMasterProcessChannelProcessorFactory.class);
323
324 assertThat(factory.canUse("tcp://localhost:" + serverPort)).isTrue();
325
326 assertThat(factory.canUse("-- whatever --")).isFalse();
327
328 errorCollector.checkThrows(MalformedURLException.class, new ThrowingRunnable() {
329 @Override
330 public void run() throws Throwable {
331 factory.connect("pipe://1");
332 fail("should not connect");
333 }
334 });
335
336 errorCollector.checkThrows(IOException.class, new ThrowingRunnable() {
337 @Override
338 public void run() throws Throwable {
339 factory.connect("tcp://localhost:123\u0000\u0000\u0000");
340 fail("should not connect to incorrect uri");
341 }
342 });
343
344 factory.connect("tcp://localhost:" + serverPort);
345 ForkNodeArguments args = new ForkedNodeArg(1, false);
346 MasterProcessChannelDecoder decoder = factory.createDecoder(args);
347 assertThat(decoder).isInstanceOf(CommandChannelDecoder.class);
348 MasterProcessChannelEncoder encoder = factory.createEncoder(args);
349 assertThat(encoder).isInstanceOf(EventChannelEncoder.class);
350 }
351 }
352 }
353
354 @Test
355 public void shouldAuthenticate() throws Exception {
356 mockStatic(ForkedBooter.class);
357
358 doCallRealMethod().when(ForkedBooter.class, "lookupDecoderFactory", anyString());
359
360 try (ServerSocketChannel server = ServerSocketChannel.open()) {
361 server.bind(new InetSocketAddress(0));
362 int serverPort = ((InetSocketAddress) server.getLocalAddress()).getPort();
363 final String uuid = UUID.randomUUID().toString();
364 String url = "tcp://localhost:" + serverPort + "?sessionId=" + uuid;
365 try (MasterProcessChannelProcessorFactory factory =
366 invokeMethod(ForkedBooter.class, "lookupDecoderFactory", url)) {
367 assertThat(factory).isInstanceOf(SurefireMasterProcessChannelProcessorFactory.class);
368
369 FutureTask<Boolean> task = new FutureTask<>(new Callable<Boolean>() {
370 @Override
371 public Boolean call() {
372 try {
373 SocketChannel channel = server.accept();
374 ByteBuffer bb = ByteBuffer.allocate(uuid.length());
375 int read = channel.read(bb);
376 assertThat(read).isEqualTo(uuid.length());
377 ((Buffer) bb).flip();
378 assertThat(new String(bb.array(), US_ASCII)).isEqualTo(uuid);
379 return true;
380 } catch (IOException e) {
381 return false;
382 }
383 }
384 });
385
386 Thread t = new Thread(task);
387 t.setDaemon(true);
388 t.start();
389
390 factory.connect(url);
391
392 try {
393 task.get(10, SECONDS);
394 } finally {
395 factory.close();
396 }
397 }
398 }
399 }
400
401 @Test
402 public void testFlushEventChannelOnExit() throws Exception {
403 mockStatic(ShutdownHookUtils.class);
404
405 final MasterProcessChannelEncoder eventChannel = mock(MasterProcessChannelEncoder.class);
406 ForkedBooter booter = new ForkedBooter();
407 setInternalState(booter, "eventChannel", eventChannel);
408
409 doAnswer(new Answer<Object>() {
410 @Override
411 public Object answer(InvocationOnMock invocation) {
412 Thread t = invocation.getArgument(0);
413 assertThat(t.isDaemon()).isTrue();
414 t.run();
415 verify(eventChannel, times(1)).onJvmExit();
416 return null;
417 }
418 })
419 .when(ShutdownHookUtils.class, "addShutDownHook", any(Thread.class));
420 invokeMethod(booter, "flushEventChannelOnExit");
421 }
422
423 @Test
424 public void shouldParseUUID() throws Exception {
425 UUID uuid = UUID.randomUUID();
426 URI uri = new URI("tcp://localhost:12345?sessionId=" + uuid);
427 String parsed = invokeMethod(SurefireMasterProcessChannelProcessorFactory.class, "extractSessionId", uri);
428 assertThat(parsed).isEqualTo(uuid.toString());
429 }
430
431 @Test
432 public void shouldNotParseUUID() throws Exception {
433 UUID uuid = UUID.randomUUID();
434 URI uri = new URI("tcp://localhost:12345?xxx=" + uuid);
435 String parsed = invokeMethod(SurefireMasterProcessChannelProcessorFactory.class, "extractSessionId", uri);
436 assertThat(parsed).isNull();
437 }
438 }