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