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 public void startElement( String uri, String localName, String qName, Attributes attributes )
121 throws SAXException
122 {
123 if ( valid )
124 {
125 try
126 {
127
128 if ( "testsuite".equals( qName ) )
129 {
130 defaultSuite = new ReportTestSuite();
131 currentSuite = defaultSuite;
132
133 try
134 {
135 Number time = numberFormat.parse( attributes.getValue( "time" ) );
136
137 defaultSuite.setTimeElapsed( time.floatValue() );
138 }
139 catch ( NullPointerException e )
140 {
141 consoleLogger.error( "WARNING: no time attribute found on testsuite element" );
142 }
143
144 final String name = attributes.getValue( "name" );
145 final String group = attributes.getValue( "group" );
146 defaultSuite.setFullClassName( StringUtils.isBlank( group )
147 ? name
148 : group + "." + name );
149
150 suites.add( defaultSuite );
151 classesToSuitesIndex.put( defaultSuite.getFullClassName(), suites.size() - 1 );
152 }
153 else if ( "testcase".equals( qName ) )
154 {
155 currentElement = new StringBuilder();
156
157 testCase = new ReportTestCase()
158 .setName( attributes.getValue( "name" ) );
159
160 String fullClassName = attributes.getValue( "classname" );
161
162
163 if ( fullClassName != null )
164 {
165 Integer currentSuiteIndex = classesToSuitesIndex.get( fullClassName );
166 if ( currentSuiteIndex == null )
167 {
168 currentSuite = new ReportTestSuite()
169 .setFullClassName( fullClassName );
170 suites.add( currentSuite );
171 classesToSuitesIndex.put( fullClassName, suites.size() - 1 );
172 }
173 else
174 {
175 currentSuite = suites.get( currentSuiteIndex );
176 }
177 }
178
179 final String timeAsString = attributes.getValue( "time" );
180 final Number time = StringUtils.isBlank( timeAsString ) ? 0 : numberFormat.parse( timeAsString );
181
182 testCase.setFullClassName( currentSuite.getFullClassName() )
183 .setClassName( currentSuite.getName() )
184 .setFullName( currentSuite.getFullClassName() + "." + testCase.getName() )
185 .setTime( time.floatValue() );
186
187 if ( currentSuite != defaultSuite )
188 {
189 currentSuite.setTimeElapsed( testCase.getTime() + currentSuite.getTimeElapsed() );
190 }
191 }
192 else if ( "failure".equals( qName ) )
193 {
194 testCase.setFailure( attributes.getValue( "message" ), attributes.getValue( "type" ) );
195 currentSuite.incrementNumberOfFailures();
196 }
197 else if ( "error".equals( qName ) )
198 {
199 testCase.setFailure( attributes.getValue( "message" ), attributes.getValue( "type" ) );
200 currentSuite.incrementNumberOfErrors();
201 }
202 else if ( "skipped".equals( qName ) )
203 {
204 final String message = attributes.getValue( "message" );
205 testCase.setFailure( message != null ? message : "skipped", "skipped" );
206 currentSuite.incrementNumberOfSkipped();
207 }
208 else if ( "flakyFailure".equals( qName ) || "flakyError".equals( qName ) )
209 {
210 currentSuite.incrementNumberOfFlakes();
211 }
212 else if ( "failsafe-summary".equals( qName ) )
213 {
214 valid = false;
215 }
216 }
217 catch ( ParseException e )
218 {
219 throw new SAXException( e.getMessage(), e );
220 }
221 }
222 }
223
224
225
226
227 public void endElement( String uri, String localName, String qName )
228 throws SAXException
229 {
230
231 if ( "testcase".equals( qName ) )
232 {
233 currentSuite.getTestCases().add( testCase );
234 }
235 else if ( "failure".equals( qName ) || "error".equals( qName ) )
236 {
237 testCase.setFailureDetail( currentElement.toString() )
238 .setFailureErrorLine( parseErrorLine( currentElement, testCase.getFullClassName() ) );
239 }
240 else if ( "time".equals( qName ) )
241 {
242 try
243 {
244 defaultSuite.setTimeElapsed( numberFormat.parse( currentElement.toString() ).floatValue() );
245 }
246 catch ( ParseException e )
247 {
248 throw new SAXException( e.getMessage(), e );
249 }
250 }
251
252 }
253
254
255
256
257 public void characters( char[] ch, int start, int length )
258 throws SAXException
259 {
260 assert start >= 0;
261 assert length >= 0;
262 if ( valid && isNotBlank( start, length, ch ) )
263 {
264 currentElement.append( ch, start, length );
265 }
266 }
267
268 public boolean isValid()
269 {
270 return valid;
271 }
272
273 static boolean isNotBlank( int from, int len, char... s )
274 {
275 assert from >= 0;
276 assert len >= 0;
277 if ( s != null )
278 {
279 for ( int i = 0; i < len; i++ )
280 {
281 char c = s[from++];
282 if ( c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f' )
283 {
284 return true;
285 }
286 }
287 }
288 return false;
289 }
290
291 static boolean isNumeric( StringBuilder s, final int from, final int to )
292 {
293 assert from >= 0;
294 assert from <= to;
295 for ( int i = from; i != to; )
296 {
297 if ( !Character.isDigit( s.charAt( i++ ) ) )
298 {
299 return false;
300 }
301 }
302 return from != to;
303 }
304
305 static String parseErrorLine( StringBuilder currentElement, String fullClassName )
306 {
307 final String[] linePatterns = { "at " + fullClassName + '.', "at " + fullClassName + '$' };
308 int[] indexes = lastIndexOf( currentElement, linePatterns );
309 int patternStartsAt = indexes[0];
310 if ( patternStartsAt != -1 )
311 {
312 int searchFrom = patternStartsAt + ( linePatterns[ indexes[1] ] ).length();
313 searchFrom = 1 + currentElement.indexOf( ":", searchFrom );
314 int searchTo = currentElement.indexOf( ")", searchFrom );
315 return isNumeric( currentElement, searchFrom, searchTo )
316 ? currentElement.substring( searchFrom, searchTo )
317 : "";
318 }
319 return "";
320 }
321
322 static int[] lastIndexOf( StringBuilder source, String... linePatterns )
323 {
324 int end = source.indexOf( "Caused by:" );
325 if ( end == -1 )
326 {
327 end = source.length();
328 }
329 int startsAt = -1;
330 int pattern = -1;
331 for ( int i = 0; i < linePatterns.length; i++ )
332 {
333 String linePattern = linePatterns[i];
334 int currentStartsAt = source.lastIndexOf( linePattern, end );
335 if ( currentStartsAt > startsAt )
336 {
337 startsAt = currentStartsAt;
338 pattern = i;
339 }
340 }
341 return new int[] { startsAt, pattern };
342 }
343 }