View Javadoc
1   package org.apache.maven.plugin.surefire.booterclient.output;
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 java.io.BufferedReader;
23  import java.io.IOException;
24  import java.io.StringReader;
25  import java.nio.ByteBuffer;
26  import java.util.Map;
27  import java.util.NoSuchElementException;
28  import java.util.Properties;
29  import java.util.StringTokenizer;
30  import java.util.concurrent.ConcurrentHashMap;
31  import java.util.concurrent.atomic.AtomicLong;
32  
33  import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.NotifiableTestStream;
34  import org.apache.maven.plugin.surefire.report.DefaultReporterFactory;
35  import org.apache.maven.shared.utils.cli.StreamConsumer;
36  import org.apache.maven.surefire.booter.ForkingRunListener;
37  import org.apache.maven.surefire.report.CategorizedReportEntry;
38  import org.apache.maven.surefire.report.ConsoleLogger;
39  import org.apache.maven.surefire.report.ConsoleOutputReceiver;
40  import org.apache.maven.surefire.report.ReportEntry;
41  import org.apache.maven.surefire.report.ReporterException;
42  import org.apache.maven.surefire.report.RunListener;
43  import org.apache.maven.surefire.report.StackTraceWriter;
44  import org.apache.maven.surefire.util.internal.StringUtils;
45  
46  import static org.apache.maven.surefire.booter.Shutdown.KILL;
47  
48  /**
49   * Knows how to reconstruct *all* the state transmitted over stdout by the forked process.
50   *
51   * @author Kristian Rosenvold
52   */
53  public class ForkClient
54      implements StreamConsumer
55  {
56      private static final long START_TIME_ZERO = 0L;
57      private static final long START_TIME_NEGATIVE_TIMEOUT = -1L;
58  
59      private final DefaultReporterFactory defaultReporterFactory;
60  
61      private final NotifiableTestStream notifiableTestStream;
62  
63      private final Map<Integer, RunListener> testSetReporters = new ConcurrentHashMap<Integer, RunListener>();
64  
65      private final Properties testVmSystemProperties;
66  
67      /**
68       * <t>testSetStartedAt</t> is set to non-zero after received
69       * {@link ForkingRunListener#BOOTERCODE_TESTSET_STARTING test-set}.
70       */
71      private final AtomicLong testSetStartedAt = new AtomicLong( START_TIME_ZERO );
72  
73      private volatile boolean saidGoodBye;
74  
75      private volatile StackTraceWriter errorInFork;
76  
77      public ForkClient( DefaultReporterFactory defaultReporterFactory, Properties testVmSystemProperties,
78                         NotifiableTestStream notifiableTestStream )
79      {
80          this.defaultReporterFactory = defaultReporterFactory;
81          this.testVmSystemProperties = testVmSystemProperties;
82          this.notifiableTestStream = notifiableTestStream;
83      }
84  
85      protected void stopOnNextTest()
86      {
87      }
88  
89      /**
90       * Called in concurrent Thread.
91       */
92      public final void tryToTimeout( long currentTimeMillis, int forkedProcessTimeoutInSeconds )
93      {
94          if ( forkedProcessTimeoutInSeconds > 0 )
95          {
96              final long forkedProcessTimeoutInMillis = 1000 * forkedProcessTimeoutInSeconds;
97              final long startedAt = testSetStartedAt.get();
98              if ( startedAt > START_TIME_ZERO && currentTimeMillis - startedAt >= forkedProcessTimeoutInMillis )
99              {
100                 testSetStartedAt.set( START_TIME_NEGATIVE_TIMEOUT );
101                 notifiableTestStream.shutdown( KILL );
102             }
103         }
104     }
105 
106     public final DefaultReporterFactory getDefaultReporterFactory()
107     {
108         return defaultReporterFactory;
109     }
110 
111     public final void consumeLine( String s )
112     {
113         if ( StringUtils.isNotBlank( s ) )
114         {
115             processLine( s );
116         }
117     }
118 
119     private void setCurrentStartTime()
120     {
121         if ( testSetStartedAt.get() == START_TIME_ZERO ) // JIT can optimize <= no JNI call
122         {
123             // Not necessary to call JNI library library #currentTimeMillis
124             // which may waste 10 - 30 machine cycles in callback. Callbacks should be fast.
125             testSetStartedAt.compareAndSet( START_TIME_ZERO, System.currentTimeMillis() );
126         }
127     }
128 
129     public final boolean hadTimeout()
130     {
131         return testSetStartedAt.get() == START_TIME_NEGATIVE_TIMEOUT;
132     }
133 
134     private void processLine( String s )
135     {
136         try
137         {
138             final byte operationId = (byte) s.charAt( 0 );
139             int commma = s.indexOf( ",", 3 );
140             if ( commma < 0 )
141             {
142                 System.out.println( s );
143                 return;
144             }
145             final int channelNumber = Integer.parseInt( s.substring( 2, commma ), 16 );
146             int rest = s.indexOf( ",", commma );
147             final String remaining = s.substring( rest + 1 );
148 
149             switch ( operationId )
150             {
151                 case ForkingRunListener.BOOTERCODE_TESTSET_STARTING:
152                     getOrCreateReporter( channelNumber ).testSetStarting( createReportEntry( remaining ) );
153                     setCurrentStartTime();
154                     break;
155                 case ForkingRunListener.BOOTERCODE_TESTSET_COMPLETED:
156                     getOrCreateReporter( channelNumber ).testSetCompleted( createReportEntry( remaining ) );
157                     break;
158                 case ForkingRunListener.BOOTERCODE_TEST_STARTING:
159                     getOrCreateReporter( channelNumber ).testStarting( createReportEntry( remaining ) );
160                     break;
161                 case ForkingRunListener.BOOTERCODE_TEST_SUCCEEDED:
162                     getOrCreateReporter( channelNumber ).testSucceeded( createReportEntry( remaining ) );
163                     break;
164                 case ForkingRunListener.BOOTERCODE_TEST_FAILED:
165                     getOrCreateReporter( channelNumber ).testFailed( createReportEntry( remaining ) );
166                     break;
167                 case ForkingRunListener.BOOTERCODE_TEST_SKIPPED:
168                     getOrCreateReporter( channelNumber ).testSkipped( createReportEntry( remaining ) );
169                     break;
170                 case ForkingRunListener.BOOTERCODE_TEST_ERROR:
171                     getOrCreateReporter( channelNumber ).testError( createReportEntry( remaining ) );
172                     break;
173                 case ForkingRunListener.BOOTERCODE_TEST_ASSUMPTIONFAILURE:
174                     getOrCreateReporter( channelNumber ).testAssumptionFailure( createReportEntry( remaining ) );
175                     break;
176                 case ForkingRunListener.BOOTERCODE_SYSPROPS:
177                     int keyEnd = remaining.indexOf( "," );
178                     StringBuilder key = new StringBuilder();
179                     StringBuilder value = new StringBuilder();
180                     StringUtils.unescapeString( key, remaining.substring( 0, keyEnd ) );
181                     StringUtils.unescapeString( value, remaining.substring( keyEnd + 1 ) );
182 
183                     synchronized ( testVmSystemProperties )
184                     {
185                         testVmSystemProperties.put( key.toString(), value.toString() );
186                     }
187                     break;
188                 case ForkingRunListener.BOOTERCODE_STDOUT:
189                     writeTestOutput( channelNumber, remaining, true );
190                     break;
191                 case ForkingRunListener.BOOTERCODE_STDERR:
192                     writeTestOutput( channelNumber, remaining, false );
193                     break;
194                 case ForkingRunListener.BOOTERCODE_CONSOLE:
195                     getOrCreateConsoleLogger( channelNumber ).info( createConsoleMessage( remaining ) );
196                     break;
197                 case ForkingRunListener.BOOTERCODE_NEXT_TEST:
198                     notifiableTestStream.provideNewTest();
199                     break;
200                 case ForkingRunListener.BOOTERCODE_ERROR:
201                     errorInFork = deserializeStackTraceWriter( new StringTokenizer( remaining, "," ) );
202                     break;
203                 case ForkingRunListener.BOOTERCODE_BYE:
204                     saidGoodBye = true;
205                     break;
206                 case ForkingRunListener.BOOTERCODE_STOP_ON_NEXT_TEST:
207                     stopOnNextTest();
208                     break;
209                 default:
210                     System.out.println( s );
211             }
212         }
213         catch ( NumberFormatException e )
214         {
215             // SUREFIRE-859
216             System.out.println( s );
217         }
218         catch ( NoSuchElementException e )
219         {
220             // SUREFIRE-859
221             System.out.println( s );
222         }
223         catch ( ReporterException e )
224         {
225             throw new RuntimeException( e );
226         }
227     }
228 
229     private void writeTestOutput( final int channelNumber, final String remaining, boolean isStdout )
230     {
231         int csNameEnd = remaining.indexOf( ',' );
232         String charsetName = remaining.substring( 0, csNameEnd );
233         String byteEncoded = remaining.substring( csNameEnd + 1 );
234         ByteBuffer unescaped = StringUtils.unescapeBytes( byteEncoded, charsetName );
235 
236         if ( unescaped.hasArray() )
237         {
238             byte[] convertedBytes = unescaped.array();
239 
240             getOrCreateConsoleOutputReceiver( channelNumber )
241                 .writeTestOutput( convertedBytes, unescaped.position(), unescaped.remaining(), isStdout );
242         }
243         else
244         {
245             byte[] convertedBytes = new byte[unescaped.remaining()];
246             unescaped.get( convertedBytes, 0, unescaped.remaining() );
247 
248             getOrCreateConsoleOutputReceiver( channelNumber )
249                 .writeTestOutput( convertedBytes, 0, convertedBytes.length, isStdout );
250         }
251     }
252 
253     public final void consumeMultiLineContent( String s )
254         throws IOException
255     {
256         BufferedReader stringReader = new BufferedReader( new StringReader( s ) );
257         for ( String s1 = stringReader.readLine(); s1 != null; s1 = stringReader.readLine() )
258         {
259             consumeLine( s1 );
260         }
261     }
262 
263     private String createConsoleMessage( String remaining )
264     {
265         return unescape( remaining );
266     }
267 
268     private ReportEntry createReportEntry( String untokenized )
269     {
270         StringTokenizer tokens = new StringTokenizer( untokenized, "," );
271         try
272         {
273             String source = nullableCsv( tokens.nextToken() );
274             String name = nullableCsv( tokens.nextToken() );
275             String group = nullableCsv( tokens.nextToken() );
276             String message = nullableCsv( tokens.nextToken() );
277             String elapsedStr = tokens.nextToken();
278             Integer elapsed = "null".equals( elapsedStr ) ? null : Integer.decode( elapsedStr );
279             final StackTraceWriter stackTraceWriter =
280                 tokens.hasMoreTokens() ? deserializeStackTraceWriter( tokens ) : null;
281 
282             return CategorizedReportEntry.reportEntry( source, name, group, stackTraceWriter, elapsed, message );
283         }
284         catch ( RuntimeException e )
285         {
286             throw new RuntimeException( untokenized, e );
287         }
288     }
289 
290     private StackTraceWriter deserializeStackTraceWriter( StringTokenizer tokens )
291     {
292         String stackTraceMessage = nullableCsv( tokens.nextToken() );
293         String smartStackTrace = nullableCsv( tokens.nextToken() );
294         String stackTrace = tokens.hasMoreTokens() ? nullableCsv( tokens.nextToken() ) : null;
295         return stackTrace != null
296             ? new DeserializedStacktraceWriter( stackTraceMessage, smartStackTrace, stackTrace )
297             : null;
298     }
299 
300     private String nullableCsv( String source )
301     {
302         return "null".equals( source ) ? null : unescape( source );
303     }
304 
305     private String unescape( String source )
306     {
307         StringBuilder stringBuffer = new StringBuilder( source.length() );
308         StringUtils.unescapeString( stringBuffer, source );
309         return stringBuffer.toString();
310     }
311 
312     /**
313      * Used when getting reporters on the plugin side of a fork.
314      *
315      * @param channelNumber The logical channel number
316      * @return A mock provider reporter
317      */
318     public final RunListener getReporter( int channelNumber )
319     {
320         return testSetReporters.get( channelNumber );
321     }
322 
323     private RunListener getOrCreateReporter( int channelNumber )
324     {
325         RunListener reporter = testSetReporters.get( channelNumber );
326         if ( reporter == null )
327         {
328             reporter = defaultReporterFactory.createReporter();
329             testSetReporters.put( channelNumber, reporter );
330         }
331         return reporter;
332     }
333 
334     private ConsoleOutputReceiver getOrCreateConsoleOutputReceiver( int channelNumber )
335     {
336         return (ConsoleOutputReceiver) getOrCreateReporter( channelNumber );
337     }
338 
339     private ConsoleLogger getOrCreateConsoleLogger( int channelNumber )
340     {
341         return (ConsoleLogger) getOrCreateReporter( channelNumber );
342     }
343 
344     public void close( boolean hadTimeout )
345     {
346     }
347 
348     public final boolean isSaidGoodBye()
349     {
350         return saidGoodBye;
351     }
352 
353     public final StackTraceWriter getErrorInFork()
354     {
355         return errorInFork;
356     }
357 
358     public final boolean isErrorInFork()
359     {
360         return errorInFork != null;
361     }
362 }