blob: 80d8e79d6cf99a98578c98e6d07606c491cc5830 [file] [log] [blame]
package org.apache.maven.surefire.report;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.maven.surefire.api.report.SafeThrowable;
import java.util.ArrayList;
import java.util.List;
import static java.lang.Math.min;
import static java.util.Arrays.asList;
import static java.util.Collections.reverse;
import static org.apache.maven.surefire.shared.utils.StringUtils.chompLast;
import static org.apache.maven.surefire.shared.utils.StringUtils.isNotEmpty;
/**
* @author Kristian Rosenvold
*/
@SuppressWarnings( "ThrowableResultOfMethodCallIgnored" )
public class SmartStackTraceParser
{
private static final int MAX_LINE_LENGTH = 77;
private final SafeThrowable throwable;
private final StackTraceElement[] stackTrace;
private final String testClassName;
private final Class<?> testClass;
private final String testMethodName;
public SmartStackTraceParser( String testClassName, Throwable throwable, String testMethodName )
{
this.testMethodName = testMethodName;
this.testClassName = testClassName;
testClass = toClass( testClassName );
this.throwable = new SafeThrowable( throwable );
stackTrace = throwable.getStackTrace();
}
private static Class<?> toClass( String name )
{
try
{
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
return classLoader == null ? null : classLoader.loadClass( name );
}
catch ( ClassNotFoundException e )
{
return null;
}
}
private static String toSimpleClassName( String className )
{
int i = className.lastIndexOf( "." );
return className.substring( i + 1 );
}
@SuppressWarnings( "ThrowableResultOfMethodCallIgnored" )
public String getString()
{
if ( testClass == null )
{
return throwable.getLocalizedMessage();
}
final StringBuilder result = new StringBuilder();
final List<StackTraceElement> stackTraceElements = focusOnClass( stackTrace, testClass );
reverse( stackTraceElements );
final String testClassSimpleName = toSimpleClassName( testClassName );
if ( stackTraceElements.isEmpty() )
{
result.append( testClassSimpleName );
if ( isNotEmpty( testMethodName ) )
{
result.append( "." )
.append( testMethodName );
}
}
else
{
for ( int i = 0, size = stackTraceElements.size(); i < size; i++ )
{
final StackTraceElement stackTraceElement = stackTraceElements.get( i );
final boolean isTestClassName = stackTraceElement.getClassName().equals( testClassName );
if ( i == 0 )
{
result.append( testClassSimpleName )
.append( isTestClassName ? '.' : '>' );
}
if ( !isTestClassName )
{
result.append( toSimpleClassName( stackTraceElement.getClassName() ) )
.append( '.' );
}
result.append( stackTraceElement.getMethodName() )
.append( ':' )
.append( stackTraceElement.getLineNumber() )
.append( "->" );
}
if ( result.length() >= 2 )
{
result.setLength( result.length() - 2 );
}
}
final Throwable target = throwable.getTarget();
final Class<?> excType = target.getClass();
final String excClassName = excType.getName();
final String msg = throwable.getMessage();
if ( target instanceof AssertionError
|| "junit.framework.AssertionFailedError".equals( excClassName )
|| "junit.framework.ComparisonFailure".equals( excClassName )
|| excClassName.startsWith( "org.opentest4j." ) )
{
if ( isNotEmpty( msg ) )
{
result.append( ' ' )
.append( msg );
}
}
else
{
result.append( rootIsInclass() ? " " : " ยป " )
.append( toMinimalThrowableMiniMessage( excType ) );
result.append( truncateMessage( msg, MAX_LINE_LENGTH - result.length() ) );
}
return result.toString();
}
private static String toMinimalThrowableMiniMessage( Class<?> excType )
{
String name = excType.getSimpleName();
if ( name.endsWith( "Exception" ) )
{
return chompLast( name, "Exception" );
}
if ( name.endsWith( "Error" ) )
{
return chompLast( name, "Error" );
}
return name;
}
private static String truncateMessage( String msg, int i )
{
StringBuilder truncatedMessage = new StringBuilder();
if ( i >= 0 && msg != null )
{
truncatedMessage.append( ' ' )
.append( msg, 0, min( i, msg.length() ) );
if ( i < msg.length() )
{
truncatedMessage.append( "..." );
}
}
return truncatedMessage.toString();
}
private boolean rootIsInclass()
{
return stackTrace.length > 0 && stackTrace[0].getClassName().equals( testClassName );
}
private static List<StackTraceElement> focusOnClass( StackTraceElement[] stackTrace, Class<?> clazz )
{
List<StackTraceElement> result = new ArrayList<>();
for ( StackTraceElement element : stackTrace )
{
if ( element != null && isInSupers( clazz, element.getClassName() ) )
{
result.add( element );
}
}
return result;
}
private static boolean isInSupers( Class<?> testClass, String lookFor )
{
if ( lookFor.startsWith( "junit.framework." ) )
{
return false;
}
while ( !testClass.getName().equals( lookFor ) && testClass.getSuperclass() != null )
{
testClass = testClass.getSuperclass();
}
return testClass.getName().equals( lookFor );
}
static Throwable findTopmostWithClass( final Throwable t, StackTraceFilter filter )
{
Throwable n = t;
do
{
if ( containsClassName( n.getStackTrace(), filter ) )
{
return n;
}
n = n.getCause();
}
while ( n != null );
return t;
}
public static String stackTraceWithFocusOnClassAsString( Throwable t, String className )
{
StackTraceFilter filter = new ClassNameStackTraceFilter( className );
Throwable topmost = findTopmostWithClass( t, filter );
List<StackTraceElement> stackTraceElements = focusInsideClass( topmost.getStackTrace(), filter );
String s = causeToString( topmost.getCause(), filter );
return toString( t, stackTraceElements, filter ) + s;
}
static List<StackTraceElement> focusInsideClass( StackTraceElement[] stackTrace, StackTraceFilter filter )
{
List<StackTraceElement> result = new ArrayList<>();
for ( StackTraceElement element : stackTrace )
{
if ( filter.matches( element ) )
{
result.add( element );
}
}
return result;
}
private static boolean containsClassName( StackTraceElement[] stackTrace, StackTraceFilter filter )
{
for ( StackTraceElement element : stackTrace )
{
if ( filter.matches( element ) )
{
return true;
}
}
return false;
}
private static String causeToString( Throwable cause, StackTraceFilter filter )
{
StringBuilder resp = new StringBuilder();
while ( cause != null )
{
resp.append( "Caused by: " );
resp.append( toString( cause, asList( cause.getStackTrace() ), filter ) );
cause = cause.getCause();
}
return resp.toString();
}
private static String toString( Throwable t, Iterable<StackTraceElement> elements, StackTraceFilter filter )
{
StringBuilder result = new StringBuilder();
if ( t != null )
{
result.append( t.getClass().getName() );
String msg = t.getMessage();
if ( msg != null )
{
result.append( ": " );
if ( isMultiLine( msg ) )
{
// SUREFIRE-986
result.append( '\n' );
}
result.append( msg );
}
result.append( '\n' );
}
for ( StackTraceElement element : elements )
{
if ( filter.matches( element ) )
{
result.append( "\tat " )
.append( element )
.append( '\n' );
}
}
return result.toString();
}
private static boolean isMultiLine( String msg )
{
int countNewLines = 0;
for ( int i = 0, length = msg.length(); i < length; i++ )
{
if ( msg.charAt( i ) == '\n' )
{
if ( ++countNewLines == 2 )
{
break;
}
}
}
return countNewLines > 1 || countNewLines == 1 && !msg.trim().endsWith( "\n" );
}
}