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.spi;
20  
21  import javax.annotation.Nonnull;
22  
23  import java.io.ByteArrayInputStream;
24  import java.io.EOFException;
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.util.Random;
29  import java.util.concurrent.ConcurrentLinkedQueue;
30  
31  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
32  import org.apache.maven.surefire.api.booter.Command;
33  import org.apache.maven.surefire.api.booter.DumpErrorSingleton;
34  import org.apache.maven.surefire.api.booter.Shutdown;
35  import org.apache.maven.surefire.api.fork.ForkNodeArguments;
36  import org.apache.maven.surefire.booter.ForkedNodeArg;
37  import org.junit.After;
38  import org.junit.Before;
39  import org.junit.Rule;
40  import org.junit.Test;
41  import org.junit.rules.TemporaryFolder;
42  
43  import static java.nio.channels.Channels.newChannel;
44  import static java.nio.charset.StandardCharsets.UTF_8;
45  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.BYE_ACK;
46  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.NOOP;
47  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.RUN_CLASS;
48  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SHUTDOWN;
49  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.SKIP_SINCE_NEXT_TEST;
50  import static org.apache.maven.surefire.api.booter.MasterProcessCommand.TEST_SET_FINISHED;
51  import static org.apache.maven.surefire.api.booter.Shutdown.DEFAULT;
52  import static org.apache.maven.surefire.api.booter.Shutdown.EXIT;
53  import static org.apache.maven.surefire.api.booter.Shutdown.KILL;
54  import static org.assertj.core.api.Assertions.assertThat;
55  import static org.junit.Assert.assertEquals;
56  import static org.junit.Assert.assertNull;
57  import static org.junit.Assert.fail;
58  
59  /**
60   * Tests for {@link CommandChannelDecoder}.
61   */
62  @SuppressWarnings("checkstyle:magicnumber")
63  public class CommandChannelDecoderTest {
64      private static final Random RND = new Random();
65  
66      @Rule
67      public final TemporaryFolder tempFolder = new TemporaryFolder();
68  
69      @Before
70      public void initTmpFile() {
71          File reportsDir = tempFolder.getRoot();
72          String dumpFileName = "surefire-" + RND.nextLong();
73          DumpErrorSingleton.getSingleton().init(reportsDir, dumpFileName);
74      }
75  
76      @After
77      public void deleteTmpFiles() {
78          tempFolder.delete();
79      }
80  
81      @Test
82      public void testDecoderRunClass() throws IOException {
83          assertEquals(String.class, RUN_CLASS.getDataType());
84          byte[] encoded = new StringBuilder(512)
85                  .append(":maven-surefire-command:")
86                  .append((char) 13)
87                  .append(":run-testclass:")
88                  .append((char) 5)
89                  .append(":UTF-8:")
90                  .append((char) 0)
91                  .append((char) 0)
92                  .append((char) 0)
93                  .append((char) 8)
94                  .append(":")
95                  .append("pkg.Test")
96                  .append(":")
97                  .toString()
98                  .getBytes(UTF_8);
99          InputStream is = new ByteArrayInputStream(encoded);
100         ForkNodeArguments args = new ForkedNodeArg(1, false);
101         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
102         Command command = decoder.decode();
103         assertThat(command.getCommandType()).isSameAs(RUN_CLASS);
104         assertThat(command.getData()).isEqualTo("pkg.Test");
105     }
106 
107     @Test
108     public void testDecoderTestsetFinished() throws IOException {
109         Command command = Command.TEST_SET_FINISHED;
110         assertThat(command.getCommandType()).isSameAs(TEST_SET_FINISHED);
111         assertEquals(Void.class, TEST_SET_FINISHED.getDataType());
112         byte[] encoded = ":maven-surefire-command:\u0010:testset-finished:".getBytes();
113         ByteArrayInputStream is = new ByteArrayInputStream(encoded);
114         ForkNodeArguments args = new ForkedNodeArg(1, false);
115         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
116         command = decoder.decode();
117         assertThat(command.getCommandType()).isSameAs(TEST_SET_FINISHED);
118         assertNull(command.getData());
119     }
120 
121     @Test
122     public void testDecoderSkipSinceNextTest() throws IOException {
123         Command command = Command.SKIP_SINCE_NEXT_TEST;
124         assertThat(command.getCommandType()).isSameAs(SKIP_SINCE_NEXT_TEST);
125         assertEquals(Void.class, SKIP_SINCE_NEXT_TEST.getDataType());
126         byte[] encoded = ":maven-surefire-command:\u0014:skip-since-next-test:".getBytes();
127         ByteArrayInputStream is = new ByteArrayInputStream(encoded);
128         ForkNodeArguments args = new ForkedNodeArg(1, false);
129         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
130         command = decoder.decode();
131         assertThat(command.getCommandType()).isSameAs(SKIP_SINCE_NEXT_TEST);
132         assertNull(command.getData());
133     }
134 
135     @Test
136     public void testDecoderShutdownWithExit() throws IOException {
137         Shutdown shutdownType = EXIT;
138         assertEquals(String.class, SHUTDOWN.getDataType());
139         byte[] encoded = (":maven-surefire-command:\u0008:shutdown:\u0005:UTF-8:\u0000\u0000\u0000\u0004:"
140                         + shutdownType.getParam() + ":")
141                 .getBytes();
142         ByteArrayInputStream is = new ByteArrayInputStream(encoded);
143         ForkNodeArguments args = new ForkedNodeArg(1, false);
144         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
145         Command command = decoder.decode();
146         assertThat(command.getCommandType()).isSameAs(SHUTDOWN);
147         assertThat(command.getData()).isEqualTo(shutdownType.name());
148     }
149 
150     @Test
151     public void testDecoderShutdownWithKill() throws IOException {
152         Shutdown shutdownType = KILL;
153         assertEquals(String.class, SHUTDOWN.getDataType());
154         byte[] encoded = (":maven-surefire-command:\u0008:shutdown:\u0005:UTF-8:\u0000\u0000\u0000\u0004:"
155                         + shutdownType.getParam() + ":")
156                 .getBytes();
157         ByteArrayInputStream is = new ByteArrayInputStream(encoded);
158         ForkNodeArguments args = new ForkedNodeArg(1, false);
159         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
160         Command command = decoder.decode();
161         assertThat(command.getCommandType()).isSameAs(SHUTDOWN);
162         assertThat(command.getData()).isEqualTo(shutdownType.name());
163     }
164 
165     @Test
166     public void testDecoderShutdownWithDefault() throws IOException {
167         Shutdown shutdownType = DEFAULT;
168         assertEquals(String.class, SHUTDOWN.getDataType());
169         byte[] encoded = (":maven-surefire-command:\u0008:shutdown:\u0005:UTF-8:\u0000\u0000\u0000\u0007:"
170                         + shutdownType.getParam() + ":")
171                 .getBytes();
172         ByteArrayInputStream is = new ByteArrayInputStream(encoded);
173         ForkNodeArguments args = new ForkedNodeArg(1, false);
174         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
175         Command command = decoder.decode();
176         assertThat(command.getCommandType()).isSameAs(SHUTDOWN);
177         assertThat(command.getData()).isEqualTo(shutdownType.name());
178     }
179 
180     @Test
181     public void testDecoderNoop() throws IOException {
182         assertThat(NOOP).isSameAs(Command.NOOP.getCommandType());
183         assertEquals(Void.class, NOOP.getDataType());
184         byte[] encoded = ":maven-surefire-command:\u0004:noop:".getBytes();
185         ByteArrayInputStream is = new ByteArrayInputStream(encoded);
186         ForkNodeArguments args = new ForkedNodeArg(1, false);
187         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
188         Command command = decoder.decode();
189         assertThat(command.getCommandType()).isSameAs(NOOP);
190         assertNull(command.getData());
191     }
192 
193     @Test
194     public void shouldIgnoreDamagedStream() throws IOException {
195         assertThat(BYE_ACK).isSameAs(Command.BYE_ACK.getCommandType());
196         assertEquals(Void.class, BYE_ACK.getDataType());
197         byte[] encoded = ":maven-surefire-command:\u0007:bye-ack:".getBytes();
198         byte[] streamContent = ("<something>" + new String(encoded) + "<damaged>").getBytes();
199         ByteArrayInputStream is = new ByteArrayInputStream(streamContent);
200         ForkNodeArguments args = new ForkedNodeArg(1, false);
201         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
202         Command command = decoder.decode();
203         assertThat(command.getCommandType()).isSameAs(BYE_ACK);
204         assertNull(command.getData());
205     }
206 
207     @Test
208     public void shouldIgnoreDamagedHeader() throws IOException {
209         assertThat(BYE_ACK).isSameAs(Command.BYE_ACK.getCommandType());
210         assertEquals(Void.class, BYE_ACK.getDataType());
211         byte[] encoded = ":maven-surefire-command:\u0007:bye-ack:".getBytes();
212         byte[] streamContent = (":<damaged>:" + new String(encoded)).getBytes();
213         ByteArrayInputStream is = new ByteArrayInputStream(streamContent);
214         ForkNodeArguments args = new ForkedNodeArg(1, false);
215         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
216         Command command = decoder.decode();
217         assertThat(command.getCommandType()).isSameAs(BYE_ACK);
218         assertNull(command.getData());
219     }
220 
221     @Test
222     public void testDecoderByeAck() throws IOException {
223         assertThat(BYE_ACK).isSameAs(Command.BYE_ACK.getCommandType());
224         assertEquals(Void.class, BYE_ACK.getDataType());
225         byte[] encoded = ":maven-surefire-command:\u0007:bye-ack:".getBytes();
226         ByteArrayInputStream is = new ByteArrayInputStream(encoded);
227         ForkNodeArguments args = new ForkedNodeArg(1, false);
228         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
229         Command command = decoder.decode();
230         assertThat(command.getCommandType()).isSameAs(BYE_ACK);
231         assertNull(command.getData());
232     }
233 
234     @Test
235     public void shouldDecodeTwoCommands() throws IOException {
236         String cmd = ":maven-surefire-command:\u0007:bye-ack:\r\n:maven-surefire-command:\u0007:bye-ack:";
237         InputStream is = new ByteArrayInputStream(cmd.getBytes());
238         ForkNodeArguments args = new ForkedNodeArg(1, false);
239         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
240 
241         Command command = decoder.decode();
242         assertThat(command.getCommandType()).isEqualTo(BYE_ACK);
243         assertThat(command.getData()).isNull();
244 
245         command = decoder.decode();
246         assertThat(command.getCommandType()).isEqualTo(BYE_ACK);
247         assertThat(command.getData()).isNull();
248 
249         decoder.close();
250     }
251 
252     @Test(expected = EOFException.class)
253     public void testIncompleteCommand() throws IOException {
254 
255         ByteArrayInputStream is = new ByteArrayInputStream(":maven-surefire-command:".getBytes());
256         ForkNodeArguments args = new ForkedNodeArg(1, false);
257         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
258         decoder.decode();
259         fail();
260     }
261 
262     @Test(expected = EOFException.class)
263     public void testIncompleteCommandStart() throws IOException {
264 
265         ByteArrayInputStream is = new ByteArrayInputStream(new byte[] {':', '\r'});
266         ForkNodeArguments args = new ForkedNodeArg(1, false);
267         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
268         decoder.decode();
269         fail();
270     }
271 
272     @Test(expected = EOFException.class)
273     public void shouldNotDecodeCorruptedCommand() throws IOException {
274         String cmd = ":maven-surefire-command:\u0007:bye-ack ::maven-surefire-command:";
275         InputStream is = new ByteArrayInputStream(cmd.getBytes());
276         ForkNodeArguments args = new ForkedNodeArg(1, false);
277         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
278 
279         decoder.decode();
280     }
281 
282     @Test
283     public void shouldSkipCorruptedCommand() throws IOException {
284         String cmd = ":maven-surefire-command:\0007:bye-ack\r\n::maven-surefire-command:\u0004:noop:";
285         InputStream is = new ByteArrayInputStream(cmd.getBytes());
286         ForkNodeArguments args = new ForkedNodeArg(1, false);
287         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(is), args);
288 
289         Command command = decoder.decode();
290         assertThat(command.getCommandType()).isSameAs(NOOP);
291         assertNull(command.getData());
292     }
293 
294     @Test
295     public void testBinaryCommandStream() throws Exception {
296         InputStream commands = getClass().getResourceAsStream("/binary-commands/75171711-encoder.bin");
297         assertThat(commands).isNotNull();
298         ConsoleLoggerMock logger = new ConsoleLoggerMock(true, true, true, true);
299         ForkNodeArguments args = new ForkNodeArgumentsMock(logger, new File(""));
300         CommandChannelDecoder decoder = new CommandChannelDecoder(newChannel(commands), args);
301 
302         Command command = decoder.decode();
303         assertThat(command).isNotNull();
304         assertThat(command.getCommandType()).isEqualTo(NOOP);
305         assertThat(command.getData()).isNull();
306 
307         command = decoder.decode();
308         assertThat(command).isNotNull();
309         assertThat(command.getCommandType()).isEqualTo(RUN_CLASS);
310         assertThat(command.getData()).isEqualTo("pkg.ATest");
311 
312         for (int i = 0; i < 24; i++) {
313             command = decoder.decode();
314             assertThat(command).isNotNull();
315             assertThat(command.getCommandType()).isEqualTo(NOOP);
316             assertThat(command.getData()).isNull();
317         }
318     }
319 
320     /**
321      * Threadsafe impl. Mockito and Powermock are not thread-safe.
322      */
323     private static class ForkNodeArgumentsMock implements ForkNodeArguments {
324         private final ConcurrentLinkedQueue<String> dumpStreamText = new ConcurrentLinkedQueue<>();
325         private final ConcurrentLinkedQueue<String> logWarningAtEnd = new ConcurrentLinkedQueue<>();
326         private final ConsoleLogger logger;
327         private final File dumpStreamTextFile;
328 
329         ForkNodeArgumentsMock(ConsoleLogger logger, File dumpStreamTextFile) {
330             this.logger = logger;
331             this.dumpStreamTextFile = dumpStreamTextFile;
332         }
333 
334         @Nonnull
335         @Override
336         public String getSessionId() {
337             throw new UnsupportedOperationException();
338         }
339 
340         @Override
341         public int getForkChannelId() {
342             return 0;
343         }
344 
345         @Nonnull
346         @Override
347         public File dumpStreamText(@Nonnull String text) {
348             dumpStreamText.add(text);
349             return dumpStreamTextFile;
350         }
351 
352         @Nonnull
353         @Override
354         public File dumpStreamException(@Nonnull Throwable t) {
355             throw new UnsupportedOperationException();
356         }
357 
358         @Override
359         public void logWarningAtEnd(@Nonnull String text) {
360             logWarningAtEnd.add(text);
361         }
362 
363         @Nonnull
364         @Override
365         public ConsoleLogger getConsoleLogger() {
366             return logger;
367         }
368 
369         @Nonnull
370         @Override
371         public Object getConsoleLock() {
372             return logger;
373         }
374 
375         boolean isCalled() {
376             return !dumpStreamText.isEmpty() || !logWarningAtEnd.isEmpty();
377         }
378 
379         @Override
380         public File getEventStreamBinaryFile() {
381             return null;
382         }
383 
384         @Override
385         public File getCommandStreamBinaryFile() {
386             return null;
387         }
388     }
389 
390     /**
391      * Threadsafe impl. Mockito and Powermock are not thread-safe.
392      */
393     private static class ConsoleLoggerMock implements ConsoleLogger {
394         final ConcurrentLinkedQueue<String> debug = new ConcurrentLinkedQueue<>();
395         final ConcurrentLinkedQueue<String> info = new ConcurrentLinkedQueue<>();
396         final ConcurrentLinkedQueue<String> error = new ConcurrentLinkedQueue<>();
397         final boolean isDebug;
398         final boolean isInfo;
399         final boolean isWarning;
400         final boolean isError;
401         private volatile boolean called;
402         private volatile boolean isDebugEnabledCalled;
403         private volatile boolean isInfoEnabledCalled;
404 
405         ConsoleLoggerMock(boolean isDebug, boolean isInfo, boolean isWarning, boolean isError) {
406             this.isDebug = isDebug;
407             this.isInfo = isInfo;
408             this.isWarning = isWarning;
409             this.isError = isError;
410         }
411 
412         @Override
413         public synchronized boolean isDebugEnabled() {
414             isDebugEnabledCalled = true;
415             called = true;
416             return isDebug;
417         }
418 
419         @Override
420         public synchronized void debug(String message) {
421             debug.add(message);
422             called = true;
423         }
424 
425         @Override
426         public synchronized boolean isInfoEnabled() {
427             isInfoEnabledCalled = true;
428             called = true;
429             return isInfo;
430         }
431 
432         @Override
433         public synchronized void info(String message) {
434             info.add(message);
435             called = true;
436         }
437 
438         @Override
439         public synchronized boolean isWarnEnabled() {
440             called = true;
441             return isWarning;
442         }
443 
444         @Override
445         public synchronized void warning(String message) {
446             called = true;
447         }
448 
449         @Override
450         public synchronized boolean isErrorEnabled() {
451             called = true;
452             return isError;
453         }
454 
455         @Override
456         public synchronized void error(String message) {
457             error.add(message);
458             called = true;
459         }
460 
461         @Override
462         public synchronized void error(String message, Throwable t) {
463             called = true;
464         }
465 
466         @Override
467         public synchronized void error(Throwable t) {
468             called = true;
469         }
470 
471         synchronized boolean isCalled() {
472             return called;
473         }
474     }
475 }