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