1 package org.apache.maven.plugins.surefire.report;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.text.NumberFormat;
27 import java.text.ParseException;
28 import java.util.ArrayList;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32
33 import javax.xml.parsers.ParserConfigurationException;
34 import javax.xml.parsers.SAXParser;
35 import javax.xml.parsers.SAXParserFactory;
36
37 import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
38 import org.apache.maven.shared.utils.StringUtils;
39 import org.xml.sax.Attributes;
40 import org.xml.sax.InputSource;
41 import org.xml.sax.SAXException;
42 import org.xml.sax.helpers.DefaultHandler;
43
44 import static java.util.Locale.ENGLISH;
45
46
47
48
49 public final class TestSuiteXmlParser
50 extends DefaultHandler
51 {
52 private final NumberFormat numberFormat = NumberFormat.getInstance( ENGLISH );
53
54 private final ConsoleLogger consoleLogger;
55
56 private ReportTestSuite defaultSuite;
57
58 private ReportTestSuite currentSuite;
59
60 private Map<String, Integer> classesToSuitesIndex;
61
62 private List<ReportTestSuite> suites;
63
64 private StringBuilder currentElement;
65
66 private ReportTestCase testCase;
67
68 private boolean valid;
69
70 public TestSuiteXmlParser( ConsoleLogger consoleLogger )
71 {
72 this.consoleLogger = consoleLogger;
73 }
74
75 public List<ReportTestSuite> parse( String xmlPath )
76 throws ParserConfigurationException, SAXException, IOException
77 {
78 FileInputStream fileInputStream = new FileInputStream( new File( xmlPath ) );
79 InputStreamReader inputStreamReader = new InputStreamReader( fileInputStream, "UTF-8" );
80
81 try
82 {
83 return parse( inputStreamReader );
84 }
85 finally
86 {
87 inputStreamReader.close();
88 fileInputStream.close();
89 }
90 }
91
92 public List<ReportTestSuite> parse( InputStreamReader stream )
93 throws ParserConfigurationException, SAXException, IOException
94 {
95 SAXParserFactory factory = SAXParserFactory.newInstance();
96
97 SAXParser saxParser = factory.newSAXParser();
98
99 valid = true;
100
101 classesToSuitesIndex = new HashMap<String, Integer>();
102 suites = new ArrayList<ReportTestSuite>();
103
104 saxParser.parse( new InputSource( stream ), this );
105
106 if ( currentSuite != defaultSuite )
107 {
108 if ( defaultSuite.getNumberOfTests() == 0 )
109 {
110 suites.remove( classesToSuitesIndex.get( defaultSuite.getFullClassName() ).intValue() );
111 }
112 }
113
114 return suites;
115 }
116
117
118
119
120 @Override
121 public void startElement( String uri, String localName, String qName, Attributes attributes )
122 throws SAXException
123 {
124 if ( valid )
125 {
126 try
127 {
128
129 if ( "testsuite".equals( qName ) )
130 {
131 defaultSuite = new ReportTestSuite();
132 currentSuite = defaultSuite;
133
134 try
135 {
136 Number time = numberFormat.parse( attributes.getValue( "time" ) );
137
138 defaultSuite.setTimeElapsed( time.floatValue() );
139 }
140 catch ( NullPointerException e )
141 {
142 consoleLogger.error( "WARNING: no time attribute found on testsuite element" );
143 }
144
145 final String name = attributes.getValue( "name" );
146 final String group = attributes.getValue( "group" );
147 defaultSuite.setFullClassName( StringUtils.isBlank( group )
148 ? name
149 : group + "." + name );
150
151 suites.add( defaultSuite );
152 classesToSuitesIndex.put( defaultSuite.getFullClassName(), suites.size() - 1 );
153 }
154 else if ( "testcase".equals( qName ) )
155 {
156 currentElement = new StringBuilder();
157
158 testCase = new ReportTestCase()
159 .setName( attributes.getValue( "name" ) );
160
161 String fullClassName = attributes.getValue( "classname" );
162
163
164 if ( fullClassName != null )
165 {
166 Integer currentSuiteIndex = classesToSuitesIndex.get( fullClassName );
167 if ( currentSuiteIndex == null )
168 {
169 currentSuite = new ReportTestSuite()
170 .setFullClassName( fullClassName );
171 suites.add( currentSuite );
172 classesToSuitesIndex.put( fullClassName, suites.size() - 1 );
173 }
174 else
175 {
176 currentSuite = suites.get( currentSuiteIndex );
177 }
178 }
179
180 final String timeAsString = attributes.getValue( "time" );
181 final Number time = StringUtils.isBlank( timeAsString ) ? 0 : numberFormat.parse( timeAsString );
182
183 testCase.setFullClassName( currentSuite.getFullClassName() )
184 .setClassName( currentSuite.getName() )
185 .setFullName( currentSuite.getFullClassName() + "." + testCase.getName() )
186 .setTime( time.floatValue() );
187
188 if ( currentSuite != defaultSuite )
189 {
190 currentSuite.setTimeElapsed( testCase.getTime() + currentSuite.getTimeElapsed() );
191 }
192 }
193 else if ( "failure".equals( qName ) )
194 {
195 testCase.setFailure( attributes.getValue( "message" ), attributes.getValue( "type" ) );
196 currentSuite.incrementNumberOfFailures();
197 }
198 else if ( "error".equals( qName ) )
199 {
200 testCase.setFailure( attributes.getValue( "message" ), attributes.getValue( "type" ) );
201 currentSuite.incrementNumberOfErrors();
202 }
203 else if ( "skipped".equals( qName ) )
204 {
205 final String message = attributes.getValue( "message" );
206 testCase.setFailure( message != null ? message : "skipped", "skipped" );
207 currentSuite.incrementNumberOfSkipped();
208 }
209 else if ( "flakyFailure".equals( qName ) || "flakyError".equals( qName ) )
210 {
211 currentSuite.incrementNumberOfFlakes();
212 }
213 else if ( "failsafe-summary".equals( qName ) )
214 {
215 valid = false;
216 }
217 }
218 catch ( ParseException e )
219 {
220 throw new SAXException( e.getMessage(), e );
221 }
222 }
223 }
224
225
226
227
228 @Override
229 public void endElement( String uri, String localName, String qName )
230 throws SAXException
231 {
232
233 if ( "testcase".equals( qName ) )
234 {
235 currentSuite.getTestCases().add( testCase );
236 }
237 else if ( "failure".equals( qName ) || "error".equals( qName ) )
238 {
239 testCase.setFailureDetail( currentElement.toString() )
240 .setFailureErrorLine( parseErrorLine( currentElement, testCase.getFullClassName() ) );
241 }
242 else if ( "time".equals( qName ) )
243 {
244 try
245 {
246 defaultSuite.setTimeElapsed( numberFormat.parse( currentElement.toString() ).floatValue() );
247 }
248 catch ( ParseException e )
249 {
250 throw new SAXException( e.getMessage(), e );
251 }
252 }
253
254 }
255
256
257
258
259 @Override
260 public void characters( char[] ch, int start, int length )
261 throws SAXException
262 {
263 assert start >= 0;
264 assert length >= 0;
265 if ( valid && isNotBlank( start, length, ch ) )
266 {
267 currentElement.append( ch, start, length );
268 }
269 }
270
271 public boolean isValid()
272 {
273 return valid;
274 }
275
276 static boolean isNotBlank( int from, int len, char... s )
277 {
278 assert from >= 0;
279 assert len >= 0;
280 if ( s != null )
281 {
282 for ( int i = 0; i < len; i++ )
283 {
284 char c = s[from++];
285 if ( c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f' )
286 {
287 return true;
288 }
289 }
290 }
291 return false;
292 }
293
294 static boolean isNumeric( StringBuilder s, final int from, final int to )
295 {
296 assert from >= 0;
297 assert from <= to;
298 for ( int i = from; i != to; )
299 {
300 if ( !Character.isDigit( s.charAt( i++ ) ) )
301 {
302 return false;
303 }
304 }
305 return from != to;
306 }
307
308 static String parseErrorLine( StringBuilder currentElement, String fullClassName )
309 {
310 final String[] linePatterns = { "at " + fullClassName + '.', "at " + fullClassName + '$' };
311 int[] indexes = lastIndexOf( currentElement, linePatterns );
312 int patternStartsAt = indexes[0];
313 if ( patternStartsAt != -1 )
314 {
315 int searchFrom = patternStartsAt + ( linePatterns[ indexes[1] ] ).length();
316 searchFrom = 1 + currentElement.indexOf( ":", searchFrom );
317 int searchTo = currentElement.indexOf( ")", searchFrom );
318 return isNumeric( currentElement, searchFrom, searchTo )
319 ? currentElement.substring( searchFrom, searchTo )
320 : "";
321 }
322 return "";
323 }
324
325 static int[] lastIndexOf( StringBuilder source, String... linePatterns )
326 {
327 int end = source.indexOf( "Caused by:" );
328 if ( end == -1 )
329 {
330 end = source.length();
331 }
332 int startsAt = -1;
333 int pattern = -1;
334 for ( int i = 0; i < linePatterns.length; i++ )
335 {
336 String linePattern = linePatterns[i];
337 int currentStartsAt = source.lastIndexOf( linePattern, end );
338 if ( currentStartsAt > startsAt )
339 {
340 startsAt = currentStartsAt;
341 pattern = i;
342 }
343 }
344 return new int[] { startsAt, pattern };
345 }
346 }