1 package org.apache.maven.plugin.surefire.extensions;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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",
81 "error:",
82 "could not reserve enough space", "could not allocate", "unable to allocate",
83 "java.lang.module.findexception"
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
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
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
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
449 String encSource, String encSourceText, String encName,
450 String encNameText, String encGroup, String encMessage,
451 String encTimeElapsed,
452
453 String encTraceMessage,
454 String encSmartTrimmedStackTrace, String encStackTrace )
455 throws NumberFormatException
456 {
457 if ( encoding == null )
458 {
459
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
496
497 private enum FrameCompletion
498 {
499 NOT_COMPLETE,
500 COMPLETE,
501 MALFORMED
502 }
503 }