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