View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * PowerMock tests for {@link ForkedBooter}.
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 }