View Javadoc
1   package org.apache.maven.plugin.surefire.extensions;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.plugin.surefire.booterclient.output.DeserializedStacktraceWriter;
23  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
24  import org.apache.maven.surefire.api.booter.ForkedProcessEventType;
25  import org.apache.maven.surefire.api.event.ConsoleDebugEvent;
26  import org.apache.maven.surefire.api.event.ConsoleErrorEvent;
27  import org.apache.maven.surefire.api.event.ConsoleInfoEvent;
28  import org.apache.maven.surefire.api.event.ConsoleWarningEvent;
29  import org.apache.maven.surefire.api.event.ControlByeEvent;
30  import org.apache.maven.surefire.api.event.ControlNextTestEvent;
31  import org.apache.maven.surefire.api.event.ControlStopOnNextTestEvent;
32  import org.apache.maven.surefire.api.event.Event;
33  import org.apache.maven.surefire.api.event.JvmExitErrorEvent;
34  import org.apache.maven.surefire.api.event.StandardStreamErrEvent;
35  import org.apache.maven.surefire.api.event.StandardStreamErrWithNewLineEvent;
36  import org.apache.maven.surefire.api.event.StandardStreamOutEvent;
37  import org.apache.maven.surefire.api.event.StandardStreamOutWithNewLineEvent;
38  import org.apache.maven.surefire.api.event.SystemPropertyEvent;
39  import org.apache.maven.surefire.api.event.TestAssumptionFailureEvent;
40  import org.apache.maven.surefire.api.event.TestErrorEvent;
41  import org.apache.maven.surefire.api.event.TestFailedEvent;
42  import org.apache.maven.surefire.api.event.TestSkippedEvent;
43  import org.apache.maven.surefire.api.event.TestStartingEvent;
44  import org.apache.maven.surefire.api.event.TestSucceededEvent;
45  import org.apache.maven.surefire.api.event.TestsetCompletedEvent;
46  import org.apache.maven.surefire.api.event.TestsetStartingEvent;
47  import org.apache.maven.surefire.extensions.CloseableDaemonThread;
48  import org.apache.maven.surefire.extensions.EventHandler;
49  import org.apache.maven.surefire.extensions.ForkNodeArguments;
50  import org.apache.maven.surefire.extensions.util.CountdownCloseable;
51  import org.apache.maven.surefire.api.report.RunMode;
52  import org.apache.maven.surefire.api.report.StackTraceWriter;
53  import org.apache.maven.surefire.api.report.TestSetReportEntry;
54  import org.apache.maven.surefire.shared.codec.binary.Base64;
55  
56  import javax.annotation.Nonnull;
57  import java.io.File;
58  import java.io.IOException;
59  import java.nio.ByteBuffer;
60  import java.nio.channels.ReadableByteChannel;
61  import java.nio.charset.Charset;
62  import java.util.ArrayList;
63  import java.util.Collections;
64  import java.util.Iterator;
65  import java.util.List;
66  
67  import static java.nio.charset.StandardCharsets.US_ASCII;
68  import static org.apache.maven.surefire.api.booter.ForkedProcessEventType.MAGIC_NUMBER;
69  import static org.apache.maven.surefire.api.report.CategorizedReportEntry.reportEntry;
70  import static org.apache.maven.surefire.api.report.RunMode.MODES;
71  
72  /**
73   *
74   */
75  public class EventConsumerThread extends CloseableDaemonThread
76  {
77      private static final String[] JVM_ERROR_PATTERNS =
78          {
79              "could not create the java virtual machine",
80              "error occurred during initialization", // of VM, of boot layer
81              "error:", // general errors
82              "could not reserve enough space", "could not allocate", "unable to allocate", // memory errors
83              "java.lang.module.findexception" // JPMS errors
84          };
85      private static final String PRINTABLE_JVM_NATIVE_STREAM = "Listening for transport dt_socket at address:";
86      private static final Base64 BASE64 = new Base64();
87  
88      private final ReadableByteChannel channel;
89      private final EventHandler<Event> eventHandler;
90      private final CountdownCloseable countdownCloseable;
91      private final ForkNodeArguments arguments;
92      private volatile boolean disabled;
93  
94      public EventConsumerThread( @Nonnull String threadName,
95                                  @Nonnull ReadableByteChannel channel,
96                                  @Nonnull EventHandler<Event> eventHandler,
97                                  @Nonnull CountdownCloseable countdownCloseable,
98                                  @Nonnull ForkNodeArguments arguments )
99      {
100         super( threadName );
101         this.channel = channel;
102         this.eventHandler = eventHandler;
103         this.countdownCloseable = countdownCloseable;
104         this.arguments = arguments;
105     }
106 
107     @Override
108     public void run()
109     {
110         try ( ReadableByteChannel stream = channel;
111               CountdownCloseable c = countdownCloseable; )
112         {
113             decode();
114         }
115         catch ( IOException e )
116         {
117             // not needed
118         }
119     }
120 
121     @Override
122     public void disable()
123     {
124         disabled = true;
125     }
126 
127     @Override
128     public void close() throws IOException
129     {
130         channel.close();
131     }
132 
133     @SuppressWarnings( "checkstyle:innerassignment" )
134     private void decode() throws IOException
135     {
136         List<String> tokens = new ArrayList<>();
137         StringBuilder line = new StringBuilder();
138         StringBuilder token = new StringBuilder( MAGIC_NUMBER.length() );
139         ByteBuffer buffer = ByteBuffer.allocate( 1024 );
140         buffer.position( buffer.limit() );
141         boolean streamContinues;
142 
143         start:
144         do
145         {
146             line.setLength( 0 );
147             tokens.clear();
148             token.setLength( 0 );
149             FrameCompletion completion = null;
150             for ( boolean frameStarted = false; streamContinues = read( buffer ); completion = null )
151             {
152                 char c = (char) buffer.get();
153 
154                 if ( c == '\n' || c == '\r' )
155                 {
156                     printExistingLine( line );
157                     continue start;
158                 }
159 
160                 line.append( c );
161 
162                 if ( !frameStarted )
163                 {
164                     if ( c == ':' )
165                     {
166                         frameStarted = true;
167                         token.setLength( 0 );
168                         tokens.clear();
169                     }
170                 }
171                 else
172                 {
173                     if ( c == ':' )
174                     {
175                         tokens.add( token.toString() );
176                         token.setLength( 0 );
177                         completion = frameCompleteness( tokens );
178                         if ( completion == FrameCompletion.COMPLETE )
179                         {
180                             line.setLength( 0 );
181                             break;
182                         }
183                         else if ( completion == FrameCompletion.MALFORMED )
184                         {
185                             printExistingLine( line );
186                             continue start;
187                         }
188                     }
189                     else
190                     {
191                         token.append( c );
192                     }
193                 }
194             }
195 
196             if ( completion == FrameCompletion.COMPLETE )
197             {
198                 Event event = toEvent( tokens );
199                 if ( !disabled && event != null )
200                 {
201                     eventHandler.handleEvent( event );
202                 }
203             }
204 
205             if ( !streamContinues )
206             {
207                 printExistingLine( line );
208                 return;
209             }
210         }
211         while ( true );
212     }
213 
214     private boolean read( ByteBuffer buffer ) throws IOException
215     {
216         if ( buffer.hasRemaining() && buffer.position() > 0 )
217         {
218             return true;
219         }
220         else
221         {
222             buffer.clear();
223             boolean isEndOfStream = channel.read( buffer ) == -1;
224             buffer.flip();
225             return !isEndOfStream;
226         }
227     }
228 
229     private void printExistingLine( StringBuilder line )
230     {
231         if ( line.length() != 0 )
232         {
233             ConsoleLogger logger = arguments.getConsoleLogger();
234             String s = line.toString().trim();
235             if ( s.contains( PRINTABLE_JVM_NATIVE_STREAM ) )
236             {
237                 if ( logger.isDebugEnabled() )
238                 {
239                     logger.debug( s );
240                 }
241                 else if ( logger.isInfoEnabled() )
242                 {
243                     logger.info( s );
244                 }
245                 else
246                 {
247                     // In case of debugging forked JVM, see PRINTABLE_JVM_NATIVE_STREAM.
248                     System.out.println( s );
249                 }
250             }
251             else
252             {
253                 if ( isJvmError( s ) )
254                 {
255                     logger.error( s );
256                 }
257                 String msg = "Corrupted STDOUT by directly writing to native stream in forked JVM "
258                     + arguments.getForkChannelId() + ".";
259                 File dumpFile = arguments.dumpStreamText( msg + " Stream '" + s + "'." );
260                 arguments.logWarningAtEnd( msg + " See FAQ web page and the dump file " + dumpFile.getAbsolutePath() );
261 
262                 if ( logger.isDebugEnabled() )
263                 {
264                     logger.debug( s );
265                 }
266             }
267         }
268     }
269 
270     private Event toEvent( List<String> tokensInFrame )
271     {
272         Iterator<String> tokens = tokensInFrame.iterator();
273         String header = tokens.next();
274         assert header != null;
275 
276         ForkedProcessEventType event = ForkedProcessEventType.byOpcode( tokens.next() );
277 
278         if ( event == null )
279         {
280             return null;
281         }
282 
283         if ( event.isControlCategory() )
284         {
285             switch ( event )
286             {
287                 case BOOTERCODE_BYE:
288                     return new ControlByeEvent();
289                 case BOOTERCODE_STOP_ON_NEXT_TEST:
290                     return new ControlStopOnNextTestEvent();
291                 case BOOTERCODE_NEXT_TEST:
292                     return new ControlNextTestEvent();
293                 default:
294                     throw new IllegalStateException( "Unknown enum " + event );
295             }
296         }
297         else if ( event.isConsoleErrorCategory() || event.isJvmExitError() )
298         {
299             Charset encoding = Charset.forName( tokens.next() );
300             StackTraceWriter stackTraceWriter = decodeTrace( encoding, tokens.next(), tokens.next(), tokens.next() );
301             return event.isConsoleErrorCategory()
302                 ? new ConsoleErrorEvent( stackTraceWriter )
303                 : new JvmExitErrorEvent( stackTraceWriter );
304         }
305         else if ( event.isConsoleCategory() )
306         {
307             Charset encoding = Charset.forName( tokens.next() );
308             String msg = decode( tokens.next(), encoding );
309             switch ( event )
310             {
311                 case BOOTERCODE_CONSOLE_INFO:
312                     return new ConsoleInfoEvent( msg );
313                 case BOOTERCODE_CONSOLE_DEBUG:
314                     return new ConsoleDebugEvent( msg );
315                 case BOOTERCODE_CONSOLE_WARNING:
316                     return new ConsoleWarningEvent( msg );
317                 default:
318                     throw new IllegalStateException( "Unknown enum " + event );
319             }
320         }
321         else if ( event.isStandardStreamCategory() )
322         {
323             RunMode mode = MODES.get( tokens.next() );
324             Charset encoding = Charset.forName( tokens.next() );
325             String output = decode( tokens.next(), encoding );
326             switch ( event )
327             {
328                 case BOOTERCODE_STDOUT:
329                     return new StandardStreamOutEvent( mode, output );
330                 case BOOTERCODE_STDOUT_NEW_LINE:
331                     return new StandardStreamOutWithNewLineEvent( mode, output );
332                 case BOOTERCODE_STDERR:
333                     return new StandardStreamErrEvent( mode, output );
334                 case BOOTERCODE_STDERR_NEW_LINE:
335                     return new StandardStreamErrWithNewLineEvent( mode, output );
336                 default:
337                     throw new IllegalStateException( "Unknown enum " + event );
338             }
339         }
340         else if ( event.isSysPropCategory() )
341         {
342             RunMode mode = MODES.get( tokens.next() );
343             Charset encoding = Charset.forName( tokens.next() );
344             String key = decode( tokens.next(), encoding );
345             String value = decode( tokens.next(), encoding );
346             return new SystemPropertyEvent( mode, key, value );
347         }
348         else if ( event.isTestCategory() )
349         {
350             RunMode mode = MODES.get( tokens.next() );
351             Charset encoding = Charset.forName( tokens.next() );
352             TestSetReportEntry reportEntry =
353                 decodeReportEntry( encoding, tokens.next(), tokens.next(), tokens.next(), tokens.next(),
354                     tokens.next(), tokens.next(), tokens.next(), tokens.next(), tokens.next(), tokens.next() );
355 
356             switch ( event )
357             {
358                 case BOOTERCODE_TESTSET_STARTING:
359                     return new TestsetStartingEvent( mode, reportEntry );
360                 case BOOTERCODE_TESTSET_COMPLETED:
361                     return new TestsetCompletedEvent( mode, reportEntry );
362                 case BOOTERCODE_TEST_STARTING:
363                     return new TestStartingEvent( mode, reportEntry );
364                 case BOOTERCODE_TEST_SUCCEEDED:
365                     return new TestSucceededEvent( mode, reportEntry );
366                 case BOOTERCODE_TEST_FAILED:
367                     return new TestFailedEvent( mode, reportEntry );
368                 case BOOTERCODE_TEST_SKIPPED:
369                     return new TestSkippedEvent( mode, reportEntry );
370                 case BOOTERCODE_TEST_ERROR:
371                     return new TestErrorEvent( mode, reportEntry );
372                 case BOOTERCODE_TEST_ASSUMPTIONFAILURE:
373                     return new TestAssumptionFailureEvent( mode, reportEntry );
374                 default:
375                     throw new IllegalStateException( "Unknown enum " + event );
376             }
377         }
378 
379         throw new IllegalStateException( "Missing a branch for the event type " + event );
380     }
381 
382     private static FrameCompletion frameCompleteness( List<String> tokens )
383     {
384         if ( !tokens.isEmpty() && !MAGIC_NUMBER.equals( tokens.get( 0 ) ) )
385         {
386             return FrameCompletion.MALFORMED;
387         }
388 
389         if ( tokens.size() >= 2 )
390         {
391             String opcode = tokens.get( 1 );
392             ForkedProcessEventType event = ForkedProcessEventType.byOpcode( opcode );
393             if ( event == null )
394             {
395                 return FrameCompletion.MALFORMED;
396             }
397             else if ( event.isControlCategory() )
398             {
399                 return FrameCompletion.COMPLETE;
400             }
401             else if ( event.isConsoleErrorCategory() )
402             {
403                 return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
404             }
405             else if ( event.isConsoleCategory() )
406             {
407                 return tokens.size() == 4 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
408             }
409             else if ( event.isStandardStreamCategory() )
410             {
411                 return tokens.size() == 5 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
412             }
413             else if ( event.isSysPropCategory() )
414             {
415                 return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
416             }
417             else if ( event.isTestCategory() )
418             {
419                 return tokens.size() == 14 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
420             }
421             else if ( event.isJvmExitError() )
422             {
423                 return tokens.size() == 6 ? FrameCompletion.COMPLETE : FrameCompletion.NOT_COMPLETE;
424             }
425         }
426         return FrameCompletion.NOT_COMPLETE;
427     }
428 
429     static String decode( String line, Charset encoding )
430     {
431         // ForkedChannelEncoder is encoding the stream with US_ASCII
432         return line == null || "-".equals( line )
433             ? null
434             : new String( BASE64.decode( line.getBytes( US_ASCII ) ), encoding );
435     }
436 
437     private static StackTraceWriter decodeTrace( Charset encoding, String encTraceMessage,
438                                                  String encSmartTrimmedStackTrace, String encStackTrace )
439     {
440         String traceMessage = decode( encTraceMessage, encoding );
441         String stackTrace = decode( encStackTrace, encoding );
442         String smartTrimmedStackTrace = decode( encSmartTrimmedStackTrace, encoding );
443         boolean exists = traceMessage != null || stackTrace != null || smartTrimmedStackTrace != null;
444         return exists ? new DeserializedStacktraceWriter( traceMessage, smartTrimmedStackTrace, stackTrace ) : null;
445     }
446 
447     static TestSetReportEntry decodeReportEntry( Charset encoding,
448                                                  // ReportEntry:
449                                                  String encSource, String encSourceText, String encName,
450                                                  String encNameText, String encGroup, String encMessage,
451                                                  String encTimeElapsed,
452                                                  // StackTraceWriter:
453                                                  String encTraceMessage,
454                                                  String encSmartTrimmedStackTrace, String encStackTrace )
455         throws NumberFormatException
456     {
457         if ( encoding == null )
458         {
459             // corrupted or incomplete stream
460             return null;
461         }
462 
463         String source = decode( encSource, encoding );
464         String sourceText = decode( encSourceText, encoding );
465         String name = decode( encName, encoding );
466         String nameText = decode( encNameText, encoding );
467         String group = decode( encGroup, encoding );
468         StackTraceWriter stackTraceWriter =
469             decodeTrace( encoding, encTraceMessage, encSmartTrimmedStackTrace, encStackTrace );
470         Integer elapsed = decodeToInteger( encTimeElapsed );
471         String message = decode( encMessage, encoding );
472         return reportEntry( source, sourceText, name, nameText,
473             group, stackTraceWriter, elapsed, message, Collections.<String, String>emptyMap() );
474     }
475 
476     static Integer decodeToInteger( String line )
477     {
478         return line == null || "-".equals( line ) ? null : Integer.decode( line );
479     }
480 
481     private static boolean isJvmError( String line )
482     {
483         String lineLower = line.toLowerCase();
484         for ( String errorPattern : JVM_ERROR_PATTERNS )
485         {
486             if ( lineLower.contains( errorPattern ) )
487             {
488                 return true;
489             }
490         }
491         return false;
492     }
493 
494     /**
495      * Determines whether the frame is complete or malformed.
496      */
497     private enum FrameCompletion
498     {
499         NOT_COMPLETE,
500         COMPLETE,
501         MALFORMED
502     }
503 }