View Javadoc
1   package org.apache.maven.surefire.booter;
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.log.api.ConsoleLogger;
23  import org.apache.maven.plugin.surefire.log.api.ConsoleLoggerUtils;
24  import org.apache.maven.surefire.report.ConsoleOutputReceiver;
25  import org.apache.maven.surefire.report.ConsoleStream;
26  import org.apache.maven.surefire.report.ReportEntry;
27  import org.apache.maven.surefire.report.RunListener;
28  import org.apache.maven.surefire.report.SafeThrowable;
29  import org.apache.maven.surefire.report.SimpleReportEntry;
30  import org.apache.maven.surefire.report.StackTraceWriter;
31  import org.apache.maven.surefire.report.TestSetReportEntry;
32  import org.apache.maven.surefire.util.internal.StringUtils.EncodedArray;
33  
34  import java.io.PrintStream;
35  import java.util.Map.Entry;
36  
37  import static java.lang.Integer.toHexString;
38  import static java.nio.charset.Charset.defaultCharset;
39  import static org.apache.maven.surefire.util.internal.ObjectUtils.systemProps;
40  import static org.apache.maven.surefire.util.internal.ObjectUtils.useNonNull;
41  import static org.apache.maven.surefire.util.internal.StringUtils.encodeStringForForkCommunication;
42  import static org.apache.maven.surefire.util.internal.StringUtils.escapeBytesToPrintable;
43  import static org.apache.maven.surefire.util.internal.StringUtils.escapeToPrintable;
44  
45  /**
46   * Encodes the full output of the test run to the stdout stream.
47   * <br>
48   * This class and the ForkClient contain the full definition of the
49   * "wire-level" protocol used by the forked process. The protocol
50   * is *not* part of any public api and may change without further
51   * notice.
52   * <br>
53   * This class is threadsafe.
54   * <br>
55   * The synchronization in the underlying PrintStream (target instance)
56   * is used to preserve thread safety of the output stream. To perform
57   * multiple writes/prints for a single request, they must
58   * synchronize on "target" variable in this class.
59   *
60   * @author Kristian Rosenvold
61   */
62  public class ForkingRunListener
63      implements RunListener, ConsoleLogger, ConsoleOutputReceiver, ConsoleStream
64  {
65      public static final byte BOOTERCODE_TESTSET_STARTING = (byte) '1';
66  
67      public static final byte BOOTERCODE_TESTSET_COMPLETED = (byte) '2';
68  
69      public static final byte BOOTERCODE_STDOUT = (byte) '3';
70  
71      public static final byte BOOTERCODE_STDERR = (byte) '4';
72  
73      public static final byte BOOTERCODE_TEST_STARTING = (byte) '5';
74  
75      public static final byte BOOTERCODE_TEST_SUCCEEDED = (byte) '6';
76  
77      public static final byte BOOTERCODE_TEST_ERROR = (byte) '7';
78  
79      public static final byte BOOTERCODE_TEST_FAILED = (byte) '8';
80  
81      public static final byte BOOTERCODE_TEST_SKIPPED = (byte) '9';
82  
83      public static final byte BOOTERCODE_TEST_ASSUMPTIONFAILURE = (byte) 'G';
84  
85      /**
86       * INFO logger
87       * @see ConsoleLogger#info(String)
88       */
89      public static final byte BOOTERCODE_CONSOLE = (byte) 'H';
90  
91      public static final byte BOOTERCODE_SYSPROPS = (byte) 'I';
92  
93      public static final byte BOOTERCODE_NEXT_TEST = (byte) 'N';
94  
95      public static final byte BOOTERCODE_STOP_ON_NEXT_TEST = (byte) 'S';
96  
97      /**
98       * ERROR logger
99       * @see ConsoleLogger#error(String)
100      */
101     public static final byte BOOTERCODE_ERROR = (byte) 'X';
102 
103     public static final byte BOOTERCODE_BYE = (byte) 'Z';
104 
105     /**
106      * DEBUG logger
107      * @see ConsoleLogger#debug(String)
108      */
109     public static final byte BOOTERCODE_DEBUG = (byte) 'D';
110 
111     /**
112      * WARNING logger
113      * @see ConsoleLogger#warning(String)
114      */
115     public static final byte BOOTERCODE_WARNING = (byte) 'W';
116 
117 
118     private final PrintStream target;
119 
120     private final int testSetChannelId;
121 
122     private final boolean trimStackTraces;
123 
124     private final byte[] stdOutHeader;
125 
126     private final byte[] stdErrHeader;
127 
128     public ForkingRunListener( PrintStream target, int testSetChannelId, boolean trimStackTraces )
129     {
130         this.target = target;
131         this.testSetChannelId = testSetChannelId;
132         this.trimStackTraces = trimStackTraces;
133         stdOutHeader = createHeader( BOOTERCODE_STDOUT, testSetChannelId );
134         stdErrHeader = createHeader( BOOTERCODE_STDERR, testSetChannelId );
135         sendProps();
136     }
137 
138     @Override
139     public void testSetStarting( TestSetReportEntry report )
140     {
141         encodeAndWriteToTarget( toString( BOOTERCODE_TESTSET_STARTING, report, testSetChannelId ) );
142     }
143 
144     @Override
145     public void testSetCompleted( TestSetReportEntry report )
146     {
147         encodeAndWriteToTarget( toString( BOOTERCODE_TESTSET_COMPLETED, report, testSetChannelId ) );
148     }
149 
150     @Override
151     public void testStarting( ReportEntry report )
152     {
153         encodeAndWriteToTarget( toString( BOOTERCODE_TEST_STARTING, report, testSetChannelId ) );
154     }
155 
156     @Override
157     public void testSucceeded( ReportEntry report )
158     {
159         encodeAndWriteToTarget( toString( BOOTERCODE_TEST_SUCCEEDED, report, testSetChannelId ) );
160     }
161 
162     @Override
163     public void testAssumptionFailure( ReportEntry report )
164     {
165         encodeAndWriteToTarget( toString( BOOTERCODE_TEST_ASSUMPTIONFAILURE, report, testSetChannelId ) );
166     }
167 
168     @Override
169     public void testError( ReportEntry report )
170     {
171         encodeAndWriteToTarget( toString( BOOTERCODE_TEST_ERROR, report, testSetChannelId ) );
172     }
173 
174     @Override
175     public void testFailed( ReportEntry report )
176     {
177         encodeAndWriteToTarget( toString( BOOTERCODE_TEST_FAILED, report, testSetChannelId ) );
178     }
179 
180     @Override
181     public void testSkipped( ReportEntry report )
182     {
183         encodeAndWriteToTarget( toString( BOOTERCODE_TEST_SKIPPED, report, testSetChannelId ) );
184     }
185 
186     @Override
187     public void testExecutionSkippedByUser()
188     {
189         encodeAndWriteToTarget( toString( BOOTERCODE_STOP_ON_NEXT_TEST, new SimpleReportEntry(), testSetChannelId ) );
190     }
191 
192     private void sendProps()
193     {
194         for ( Entry<String, String> entry : systemProps().entrySet() )
195         {
196             String value = entry.getValue();
197             encodeAndWriteToTarget( toPropertyString( entry.getKey(), useNonNull( value, "null" ) ) );
198         }
199     }
200 
201     @Override
202     public void writeTestOutput( byte[] buf, int off, int len, boolean stdout )
203     {
204         EncodedArray encodedArray = escapeBytesToPrintable( stdout ? stdOutHeader : stdErrHeader, buf, off, len );
205 
206         synchronized ( target ) // See notes about synchronization/thread safety in class javadoc
207         {
208             target.write( encodedArray.getArray(), 0, encodedArray.getSize() );
209             target.flush();
210             if ( target.checkError() )
211             {
212                 // We MUST NOT throw any exception from this method; otherwise we are in loop and CPU goes up:
213                 // ForkingRunListener -> Exception -> JUnit Notifier and RunListener -> ForkingRunListener -> Exception
214                 DumpErrorSingleton.getSingleton()
215                         .dumpStreamText( "Unexpected IOException with stream: " + new String( buf, off, len ) );
216             }
217         }
218     }
219 
220     public static byte[] createHeader( byte booterCode, int testSetChannel )
221     {
222         return encodeStringForForkCommunication( String.valueOf( (char) booterCode )
223                 + ','
224                 + Integer.toString( testSetChannel, 16 )
225                 + ',' + defaultCharset().name()
226                 + ',' );
227     }
228 
229     private void log( byte bootCode, String message )
230     {
231         if ( message != null )
232         {
233             StringBuilder sb = new StringBuilder( 7 + message.length() * 5 );
234             append( sb, bootCode ); comma( sb );
235             append( sb, toHexString( testSetChannelId ) ); comma( sb );
236             escapeToPrintable( sb, message );
237 
238             sb.append( '\n' );
239             encodeAndWriteToTarget( sb.toString() );
240         }
241     }
242 
243     @Override
244     public boolean isDebugEnabled()
245     {
246         return true;
247     }
248 
249     @Override
250     public void debug( String message )
251     {
252         log( BOOTERCODE_DEBUG, message );
253     }
254 
255     @Override
256     public boolean isInfoEnabled()
257     {
258         return true;
259     }
260 
261     @Override
262     public void info( String message )
263     {
264         log( BOOTERCODE_CONSOLE, message );
265     }
266 
267     @Override
268     public boolean isWarnEnabled()
269     {
270         return true;
271     }
272 
273     @Override
274     public void warning( String message )
275     {
276         log( BOOTERCODE_WARNING, message );
277     }
278 
279     @Override
280     public boolean isErrorEnabled()
281     {
282         return true;
283     }
284 
285     @Override
286     public void error( String message )
287     {
288         log( BOOTERCODE_ERROR, message );
289     }
290 
291     @Override
292     public void error( String message, Throwable t )
293     {
294         error( ConsoleLoggerUtils.toString( message, t ) );
295     }
296 
297     @Override
298     public void error( Throwable t )
299     {
300         error( null, t );
301     }
302 
303     private void encodeAndWriteToTarget( String string )
304     {
305         byte[] encodeBytes = encodeStringForForkCommunication( string );
306         synchronized ( target ) // See notes about synchronization/thread safety in class javadoc
307         {
308             target.write( encodeBytes, 0, encodeBytes.length );
309             target.flush();
310             if ( target.checkError() )
311             {
312                 // We MUST NOT throw any exception from this method; otherwise we are in loop and CPU goes up:
313                 // ForkingRunListener -> Exception -> JUnit Notifier and RunListener -> ForkingRunListener -> Exception
314                 DumpErrorSingleton.getSingleton().dumpStreamText( "Unexpected IOException: " + string );
315             }
316         }
317     }
318 
319     private String toPropertyString( String key, String value )
320     {
321         StringBuilder stringBuilder = new StringBuilder();
322 
323         append( stringBuilder, BOOTERCODE_SYSPROPS ); comma( stringBuilder );
324         append( stringBuilder, toHexString( testSetChannelId ) ); comma( stringBuilder );
325 
326         escapeToPrintable( stringBuilder, key );
327         comma( stringBuilder );
328         escapeToPrintable( stringBuilder, value );
329         stringBuilder.append( "\n" );
330         return stringBuilder.toString();
331     }
332 
333     private String toString( byte operationCode, ReportEntry reportEntry, int testSetChannelId )
334     {
335         StringBuilder stringBuilder = new StringBuilder();
336         append( stringBuilder, operationCode ); comma( stringBuilder );
337         append( stringBuilder, toHexString( testSetChannelId ) ); comma( stringBuilder );
338 
339         nullableEncoding( stringBuilder, reportEntry.getSourceName() );
340         comma( stringBuilder );
341         nullableEncoding( stringBuilder, reportEntry.getName() );
342         comma( stringBuilder );
343         nullableEncoding( stringBuilder, reportEntry.getGroup() );
344         comma( stringBuilder );
345         nullableEncoding( stringBuilder, reportEntry.getMessage() );
346         comma( stringBuilder );
347         nullableEncoding( stringBuilder, reportEntry.getElapsed() );
348         encode( stringBuilder, reportEntry.getStackTraceWriter() );
349         stringBuilder.append( "\n" );
350         return stringBuilder.toString();
351     }
352 
353     private static void comma( StringBuilder stringBuilder )
354     {
355         stringBuilder.append( "," );
356     }
357 
358     private void append( StringBuilder stringBuilder, String message )
359     {
360         stringBuilder.append( encode( message ) );
361     }
362 
363     private void append( StringBuilder stringBuilder, byte b )
364     {
365         stringBuilder.append( (char) b );
366     }
367 
368     private void nullableEncoding( StringBuilder stringBuilder, Integer source )
369     {
370         stringBuilder.append( source == null ? "null" : source.toString() );
371     }
372 
373     private String encode( String source )
374     {
375         return source;
376     }
377 
378 
379     private static void nullableEncoding( StringBuilder stringBuilder, String source )
380     {
381         if ( source == null || source.isEmpty() )
382         {
383             stringBuilder.append( "null" );
384         }
385         else
386         {
387             escapeToPrintable( stringBuilder, source );
388         }
389     }
390 
391     private void encode( StringBuilder stringBuilder, StackTraceWriter stackTraceWriter )
392     {
393         encode( stringBuilder, stackTraceWriter, trimStackTraces );
394     }
395 
396     public static void encode( StringBuilder stringBuilder, StackTraceWriter stackTraceWriter, boolean trimStackTraces )
397     {
398         if ( stackTraceWriter != null )
399         {
400             comma( stringBuilder );
401             //noinspection ThrowableResultOfMethodCallIgnored
402             final SafeThrowable throwable = stackTraceWriter.getThrowable();
403             if ( throwable != null )
404             {
405                 String message = throwable.getLocalizedMessage();
406                 nullableEncoding( stringBuilder, message );
407             }
408             comma( stringBuilder );
409             nullableEncoding( stringBuilder, stackTraceWriter.smartTrimmedStackTrace() );
410             comma( stringBuilder );
411             nullableEncoding( stringBuilder, trimStackTraces
412                 ? stackTraceWriter.writeTrimmedTraceToString()
413                 : stackTraceWriter.writeTraceToString() );
414         }
415     }
416 
417     @Override
418     public void println( String message )
419     {
420         byte[] buf = message.getBytes();
421         println( buf, 0, buf.length );
422     }
423 
424     @Override
425     public void println( byte[] buf, int off, int len )
426     {
427         writeTestOutput( buf, off, len, true );
428     }
429 }