All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tngtech.jgiven.impl.ScenarioModelBuilder Maven / Gradle / Ivy

There is a newer version: 1.3.1
Show newest version
package com.tngtech.jgiven.impl;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.tngtech.jgiven.annotation.As;
import com.tngtech.jgiven.annotation.AsProvider;
import com.tngtech.jgiven.annotation.CaseAs;
import com.tngtech.jgiven.annotation.CaseAsProvider;
import com.tngtech.jgiven.annotation.Description;
import com.tngtech.jgiven.annotation.ExtendedDescription;
import com.tngtech.jgiven.annotation.Hidden;
import com.tngtech.jgiven.annotation.IntroWord;
import com.tngtech.jgiven.annotation.IsTag;
import com.tngtech.jgiven.annotation.Pending;
import com.tngtech.jgiven.annotation.StepComment;
import com.tngtech.jgiven.attachment.Attachment;
import com.tngtech.jgiven.config.AbstractJGivenConfiguration;
import com.tngtech.jgiven.config.ConfigurationUtil;
import com.tngtech.jgiven.config.DefaultConfiguration;
import com.tngtech.jgiven.config.TagConfiguration;
import com.tngtech.jgiven.exception.JGivenWrongUsageException;
import com.tngtech.jgiven.format.ObjectFormatter;
import com.tngtech.jgiven.impl.format.ParameterFormattingUtil;
import com.tngtech.jgiven.impl.intercept.ScenarioListener;
import com.tngtech.jgiven.impl.params.DefaultAsProvider;
import com.tngtech.jgiven.impl.util.AnnotationUtil;
import com.tngtech.jgiven.impl.util.AssertionUtil;
import com.tngtech.jgiven.impl.util.ReflectionUtil;
import com.tngtech.jgiven.impl.util.WordUtil;
import com.tngtech.jgiven.report.model.ExecutionStatus;
import com.tngtech.jgiven.report.model.InvocationMode;
import com.tngtech.jgiven.report.model.NamedArgument;
import com.tngtech.jgiven.report.model.ReportModel;
import com.tngtech.jgiven.report.model.ScenarioCaseModel;
import com.tngtech.jgiven.report.model.ScenarioModel;
import com.tngtech.jgiven.report.model.StepFormatter;
import com.tngtech.jgiven.report.model.StepModel;
import com.tngtech.jgiven.report.model.StepStatus;
import com.tngtech.jgiven.report.model.Tag;
import com.tngtech.jgiven.report.model.Word;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;

public class ScenarioModelBuilder implements ScenarioListener {
    private static final Logger log = LoggerFactory.getLogger( ScenarioModelBuilder.class );

    private static final Set STACK_TRACE_FILTER = ImmutableSet
        .of( "sun.reflect", "com.tngtech.jgiven.impl.intercept", "com.tngtech.jgiven.impl.intercept", "$$EnhancerByCGLIB$$",
            "java.lang.reflect", "net.sf.cglib.proxy", "com.sun.proxy" );
    private static final boolean FILTER_STACK_TRACE = Config.config().filterStackTrace();

    private ScenarioModel scenarioModel;
    private ScenarioCaseModel scenarioCaseModel;
    private StepModel currentStep;
    private final Stack parentSteps = new Stack();

    /**
     * In case the current step is a step with nested steps, this list contains these steps
     */
    private List nestedSteps;

    private Word introWord;

    private long scenarioStartedNanos;

    private AbstractJGivenConfiguration configuration = new DefaultConfiguration();

    private ReportModel reportModel;

    public void setReportModel( ReportModel reportModel ) {
        this.reportModel = reportModel;
    }

    @Override
    public void scenarioStarted( String description ) {
        scenarioStartedNanos = System.nanoTime();
        String readableDescription = description;

        if( description.contains( "_" ) ) {
            readableDescription = description.replace( '_', ' ' );
        } else if( !description.contains( " " ) ) {
            readableDescription = WordUtil.camelCaseToCapitalizedReadableText( description );
        }

        scenarioCaseModel = new ScenarioCaseModel();

        scenarioModel = new ScenarioModel();
        scenarioModel.addCase( scenarioCaseModel );
        scenarioModel.setDescription( readableDescription );
    }

    public void addStepMethod( Method paramMethod, List arguments, InvocationMode mode, boolean hasNestedSteps ) {
        StepModel stepModel = createStepModel( paramMethod, arguments, mode );

        if( parentSteps.empty() ) {
            getCurrentScenarioCase().addStep( stepModel );
        } else {
            parentSteps.peek().addNestedStep( stepModel );
        }

        if( hasNestedSteps ) {
            parentSteps.push( stepModel );
        }
        currentStep = stepModel;
    }

    StepModel createStepModel( Method paramMethod, List arguments, InvocationMode mode ) {
        StepModel stepModel = new StepModel();

        stepModel.setName( getDescription( paramMethod ) );

        ExtendedDescription extendedDescriptionAnnotation = paramMethod.getAnnotation( ExtendedDescription.class );
        if( extendedDescriptionAnnotation != null ) {
            stepModel.setExtendedDescription( extendedDescriptionAnnotation.value() );
        }

        List nonHiddenArguments = filterHiddenArguments( arguments, paramMethod.getParameterAnnotations() );

        ParameterFormattingUtil parameterFormattingUtil = new ParameterFormattingUtil( configuration );
        List> formatters = parameterFormattingUtil.getFormatter( paramMethod.getParameterTypes(), getNames( arguments ),
            paramMethod.getParameterAnnotations() );
        stepModel.setWords( new StepFormatter( stepModel.getName(), nonHiddenArguments, formatters ).buildFormattedWords() );

        if( introWord != null ) {
            stepModel.addIntroWord( introWord );
            introWord = null;
        }

        stepModel.setStatus( mode.toStepStatus() );
        return stepModel;
    }

    private List filterHiddenArguments( List arguments, Annotation[][] parameterAnnotations ) {
        List result = Lists.newArrayList();
        for( int i = 0; i < parameterAnnotations.length; i++ ) {
            if( !AnnotationUtil.isHidden( parameterAnnotations[i] ) ) {
                result.add( arguments.get( i ) );
            }
        }
        return result;
    }

    @Override
    public void introWordAdded( String value ) {
        introWord = new Word();
        introWord.setIntroWord( true );
        introWord.setValue( value );
    }

    private void addStepComment( List arguments  ) {
        if( arguments == null || arguments.size() != 1 ) {
            throw new JGivenWrongUsageException( "A step comment method must have exactly one parameter." );
        }

        if( !( arguments.get( 0 ).getValue() instanceof String ) ) {
            throw new JGivenWrongUsageException( "The step comment method parameter must be a string." );
        }

        if( currentStep == null ) {
            throw new JGivenWrongUsageException( "A step comment must be added after the corresponding step, "
                    + "but no step has been executed yet." );
        }

        stepCommentUpdated((String) arguments.get( 0 ).getValue() );
    }


    @Override
    public void stepCommentUpdated(String comment ) {
        currentStep.setComment( comment );
    }

    private ScenarioCaseModel getCurrentScenarioCase() {
        if( scenarioCaseModel == null ) {
            scenarioStarted( "A Scenario" );
        }
        return scenarioCaseModel;
    }

    @Override
    public void stepMethodInvoked( Method method, List arguments, InvocationMode mode, boolean hasNestedSteps ) {
        if( method.isAnnotationPresent( IntroWord.class ) ) {
            introWordAdded( getDescription( method ) );
        } else if( method.isAnnotationPresent( StepComment.class ) ) {
            addStepComment( arguments );
        } else {
            addTags( method.getAnnotations() );
            addTags( method.getDeclaringClass().getAnnotations() );

            addStepMethod( method, arguments, mode, hasNestedSteps );
        }
    }

    public void setMethodName( String methodName ) {
        scenarioModel.setTestMethodName( methodName );
    }

    public void setArguments( List arguments ) {
        scenarioCaseModel.setExplicitArguments( arguments );
    }

    public void setParameterNames( List parameterNames ) {
        scenarioModel.setExplicitParameters( removeUnderlines( parameterNames ) );
    }

    private static List removeUnderlines( List parameterNames ) {
        List result = Lists.newArrayListWithCapacity( parameterNames.size() );
        for( String paramName : parameterNames ) {
            result.add( WordUtil.fromSnakeCase( paramName ) );
        }
        return result;
    }

    private String getDescription( Method paramMethod ) {
        if( paramMethod.isAnnotationPresent( Hidden.class ) ) {
            return "";
        }

        Description description = paramMethod.getAnnotation( Description.class );
        if( description != null ) {
            return description.value();
        }

        As as = paramMethod.getAnnotation( As.class );
        AsProvider provider = as != null
                ? ReflectionUtil.newInstance( as.provider() )
                : new DefaultAsProvider();
        return provider.as( as, paramMethod );
    }

    public void setStatus( ExecutionStatus status ) {
        scenarioCaseModel.setStatus( status );
    }

    public void setException( Throwable throwable ) {
        scenarioCaseModel.setErrorMessage( throwable.getClass().getName() + ": " + throwable.getMessage() );
        scenarioCaseModel.setStackTrace( getStackTrace( throwable, FILTER_STACK_TRACE ) );
    }

    private List getStackTrace( Throwable exception, boolean filterStackTrace ) {
        StackTraceElement[] stackTraceElements = exception.getStackTrace();
        ArrayList stackTrace = new ArrayList( stackTraceElements.length );

        outer:
        for( StackTraceElement element : stackTraceElements ) {
            if( filterStackTrace ) {
                for( String filter : STACK_TRACE_FILTER ) {
                    if( element.getClassName().contains( filter ) ) {
                        continue outer;
                    }
                }
            }
            stackTrace.add( element.toString() );
        }
        return stackTrace;
    }

    @Override
    public void stepMethodFailed( Throwable t ) {
        if( currentStep != null ) {
            currentStep.setStatus( StepStatus.FAILED );
        }
    }

    @Override
    public void stepMethodFinished( long durationInNanos, boolean hasNestedSteps ) {
        if( hasNestedSteps && !parentSteps.isEmpty() ) {
            currentStep = parentSteps.peek();
        }

        if( currentStep != null ) {
            currentStep.setDurationInNanos( durationInNanos );
            if( hasNestedSteps ) {
                if( currentStep.getStatus() != StepStatus.FAILED ) {
                    currentStep.setStatus( getStatusFromNestedSteps( currentStep.getNestedSteps() ) );
                }
                parentSteps.pop();
            }
        }

        if( !hasNestedSteps && !parentSteps.isEmpty() ) {
            currentStep = parentSteps.peek();
        }
    }

    private StepStatus getStatusFromNestedSteps( List nestedSteps ) {
        StepStatus status = StepStatus.PASSED;
        for( StepModel nestedModel : nestedSteps ) {
            StepStatus nestedStatus = nestedModel.getStatus();

            switch( nestedStatus ) {
                case FAILED:
                    return StepStatus.FAILED;
                case PENDING:
                    status = StepStatus.PENDING;
                    break;
            }
        }
        return status;
    }

    @Override
    public void scenarioFailed( Throwable e ) {
        setStatus( ExecutionStatus.FAILED );
        setException( e );
    }

    @Override
    public void scenarioStarted( Class testClass, Method method, List namedArguments ) {
        readConfiguration( testClass );
        readAnnotations( testClass, method );
        scenarioModel.setClassName( testClass.getName() );
        setParameterNames( getNames( namedArguments ) );

        // must come at last
        setMethodName( method.getName() );

        ParameterFormattingUtil parameterFormattingUtil = new ParameterFormattingUtil( configuration );
        List> formatter = parameterFormattingUtil.getFormatter( method.getParameterTypes(), getNames( namedArguments ),
            method.getParameterAnnotations() );

        setArguments( parameterFormattingUtil.toStringList( formatter, getValues( namedArguments ) ) );
        setCaseDescription( testClass, method, namedArguments );
    }

    private void setCaseDescription( Class testClass, Method method, List namedArguments ) {

        CaseAs annotation = null;
        if( method.isAnnotationPresent( CaseAs.class ) ) {
            annotation = method.getAnnotation( CaseAs.class );
        } else if( testClass.isAnnotationPresent( CaseAs.class ) ) {
            annotation = testClass.getAnnotation( CaseAs.class );
        }

        if( annotation != null ) {
            CaseAsProvider caseDescriptionProvider = ReflectionUtil.newInstance( annotation.provider() );
            String value = annotation.value();
            List values;
            if( annotation.formatValues() ) {
                values = scenarioCaseModel.getExplicitArguments();
            } else {
                values = getValues( namedArguments );
            }
            String caseDescription = caseDescriptionProvider.as( value, scenarioModel.getExplicitParameters(), values );
            scenarioCaseModel.setDescription( caseDescription );
        }
    }

    private List getValues( List namedArguments ) {
        List result = Lists.newArrayList();
        for( NamedArgument a : namedArguments ) {
            result.add( a.value );
        }
        return result;
    }

    private List getNames( List namedArguments ) {
        List result = Lists.newArrayList();
        for( NamedArgument a : namedArguments ) {
            result.add( a.name );
        }
        return result;
    }

    private void readConfiguration( Class testClass ) {
        configuration = ConfigurationUtil.getConfiguration( testClass );
    }

    private void readAnnotations( Class testClass, Method method ) {
        String scenarioDescription = method.getName();

        if( method.isAnnotationPresent( Description.class ) ) {
            scenarioDescription = method.getAnnotation( Description.class ).value();
        } else if( method.isAnnotationPresent( As.class ) ) {
            As as = method.getAnnotation( As.class );

            AsProvider provider = ReflectionUtil.newInstance( as.provider() );
            scenarioDescription = provider.as( as, method );
        }

        scenarioStarted( scenarioDescription );

        if( method.isAnnotationPresent( ExtendedDescription.class ) ) {
            scenarioModel.setExtendedDescription( method.getAnnotation( ExtendedDescription.class ).value() );
        }

        if( method.isAnnotationPresent( Pending.class ) || method.getDeclaringClass().isAnnotationPresent( Pending.class ) ) {
            scenarioCaseModel.setStatus( ExecutionStatus.SCENARIO_PENDING );
        }

        if( scenarioCaseModel.getCaseNr() == 1 ) {
            addTags( testClass.getAnnotations() );
            addTags( method.getAnnotations() );
        }
    }

    public void addTags( Annotation... annotations ) {
        for( Annotation annotation : annotations ) {
            addTags( toTags( annotation ) );
        }
    }

    private void addTags( List tags ) {
        if( tags.isEmpty() ) {
            return;
        }

        if( reportModel != null ) {
            this.reportModel.addTags( tags );
        }

        if( scenarioModel != null ) {
            this.scenarioModel.addTags( tags );
        }
    }

    public List toTags( Annotation annotation ) {
        Class annotationType = annotation.annotationType();
        TagConfiguration tagConfig = toTagConfiguration( annotationType );
        if( tagConfig == null ) {
            return Collections.emptyList();
        }

        return toTags( tagConfig, Optional.of( annotation ) );
    }

    private List toTags( TagConfiguration tagConfig, Optional annotation ) {
        Tag tag = new Tag( tagConfig.getAnnotationFullType() );

        tag.setType( tagConfig.getAnnotationType() );

        if( !Strings.isNullOrEmpty( tagConfig.getName() ) ) {
            tag.setName( tagConfig.getName() );
        }

        if( tagConfig.isPrependType() ) {
            tag.setPrependType( true );
        }

        tag.setShowInNavigation( tagConfig.showInNavigation() );

        if( !Strings.isNullOrEmpty( tagConfig.getCssClass() ) ) {
            tag.setCssClass( tagConfig.getCssClass() );
        }

        if( !Strings.isNullOrEmpty( tagConfig.getColor() ) ) {
            tag.setColor( tagConfig.getColor() );
        }

        if( !Strings.isNullOrEmpty( tagConfig.getStyle() ) ) {
            tag.setStyle( tagConfig.getStyle() );
        }

        Object value = tagConfig.getDefaultValue();
        if( !Strings.isNullOrEmpty( tagConfig.getDefaultValue() ) ) {
            tag.setValue( tagConfig.getDefaultValue() );
        }

        tag.setTags( tagConfig.getTags() );

        if( tagConfig.isIgnoreValue() || !annotation.isPresent() ) {
            tag.setDescription( getDescriptionFromGenerator( tagConfig, annotation.orElse( null ), tagConfig.getDefaultValue() ) );
            tag.setHref( getHref( tagConfig, annotation.orElse( null ), value ) );

            return Collections.singletonList( tag );
        }

        try {
            Method method = annotation.get().annotationType().getMethod( "value" );
            value = method.invoke( annotation.get() );
            if( value != null ) {
                if( value.getClass().isArray() ) {
                    Object[] objectArray = (Object[]) value;
                    if( tagConfig.isExplodeArray() ) {
                        List explodedTags = getExplodedTags( tag, objectArray, annotation.get(), tagConfig );
                        return explodedTags;
                    }
                    tag.setValue( toStringList( objectArray ) );
                } else {
                    tag.setValue( String.valueOf( value ) );
                }
            }
        } catch( NoSuchMethodException ignore ) {

        } catch( Exception e ) {
            log.error( "Error while getting 'value' method of annotation " + annotation.get(), e );
        }

        tag.setDescription( getDescriptionFromGenerator( tagConfig, annotation.get(), value ) );
        tag.setHref( getHref( tagConfig, annotation.get(), value ) );

        return Collections.singletonList( tag );
    }

    private TagConfiguration toTagConfiguration( Class annotationType ) {
        IsTag isTag = annotationType.getAnnotation( IsTag.class );
        if( isTag != null ) {
            return fromIsTag( isTag, annotationType );
        }

        return configuration.getTagConfiguration( annotationType );
    }

    public TagConfiguration fromIsTag( IsTag isTag, Class annotationType ) {
        String name = isTag.name();

        return TagConfiguration.builder( annotationType )
            .defaultValue( isTag.value() )
            .description( isTag.description() )
            .explodeArray( isTag.explodeArray() )
            .ignoreValue( isTag.ignoreValue() )
            .prependType( isTag.prependType() )
            .name( name )
            .descriptionGenerator( isTag.descriptionGenerator() )
            .cssClass( isTag.cssClass() )
            .color( isTag.color() )
            .style( isTag.style() )
            .tags( getTagNames( isTag, annotationType ) )
            .href( isTag.href() )
            .hrefGenerator( isTag.hrefGenerator() )
            .showInNavigation( isTag.showInNavigation() )
            .build();
    }

    private List getTagNames( IsTag isTag, Class annotationType ) {
        List tags = getTags( isTag, annotationType );
        reportModel.addTags( tags );
        List tagNames = Lists.newArrayList();
        for( Tag tag : tags ) {
            tagNames.add( tag.toIdString() );
        }
        return tagNames;
    }

    private List getTags( IsTag isTag, Class annotationType ) {
        List allTags = Lists.newArrayList();

        for( Annotation a : annotationType.getAnnotations() ) {
            if( a.annotationType().isAnnotationPresent( IsTag.class ) ) {
                List tags = toTags( a );
                for( Tag tag : tags ) {
                    allTags.add( tag );
                }
            }
        }

        return allTags;
    }

    private List toStringList( Object[] value ) {
        Object[] array = value;
        List values = Lists.newArrayList();
        for( Object v : array ) {
            values.add( String.valueOf( v ) );
        }
        return values;
    }

    private String getDescriptionFromGenerator( TagConfiguration tagConfiguration, Annotation annotation, Object value ) {
        try {
            return tagConfiguration.getDescriptionGenerator().newInstance().generateDescription( tagConfiguration, annotation, value );
        } catch( Exception e ) {
            throw new JGivenWrongUsageException(
                "Error while trying to generate the description for annotation " + annotation + " using DescriptionGenerator class "
                        + tagConfiguration.getDescriptionGenerator() + ": " + e.getMessage(),
                e );
        }
    }

    private String getHref( TagConfiguration tagConfiguration, Annotation annotation, Object value ) {
        try {
            return tagConfiguration.getHrefGenerator().newInstance().generateHref( tagConfiguration, annotation, value );
        } catch( Exception e ) {
            throw new JGivenWrongUsageException(
                "Error while trying to generate the href for annotation " + annotation + " using HrefGenerator class "
                        + tagConfiguration.getHrefGenerator() + ": " + e.getMessage(),
                e );
        }
    }

    private List getExplodedTags( Tag originalTag, Object[] values, Annotation annotation, TagConfiguration tagConfig ) {
        List result = Lists.newArrayList();
        for( Object singleValue : values ) {
            Tag newTag = originalTag.copy();
            newTag.setValue( String.valueOf( singleValue ) );
            newTag.setDescription( getDescriptionFromGenerator( tagConfig, annotation, singleValue ) );
            newTag.setHref( getHref( tagConfig, annotation, singleValue ) );
            result.add( newTag );
        }
        return result;
    }

    @Override
    public void scenarioFinished() {
        AssertionUtil.assertTrue( scenarioStartedNanos > 0, "Scenario has no start time" );
        long durationInNanos = System.nanoTime() - scenarioStartedNanos;
        scenarioCaseModel.setDurationInNanos( durationInNanos );
        scenarioModel.addDurationInNanos( durationInNanos );
        reportModel.addScenarioModelOrMergeWithExistingOne( scenarioModel );
    }

    @Override
    public void attachmentAdded( Attachment attachment ) {
        currentStep.addAttachment( attachment );
    }

    @Override
    public void extendedDescriptionUpdated( String extendedDescription ) {
        currentStep.setExtendedDescription( extendedDescription );
    }

    @Override
    public void stepNameUpdated( String newStepName ) {
        List newWords = Lists.newArrayList();

        for (Word word : currentStep.getWords()) {
            if (word.isIntroWord()) {
                newWords.add(word);
            }
        }

        newWords.add(new Word(newStepName));

        currentStep.setWords(newWords);
        currentStep.setName( newStepName );
    }

    @Override
    public void sectionAdded( String sectionTitle ) {
        StepModel stepModel = new StepModel();
        stepModel.setName( sectionTitle );
        stepModel.addWords( new Word( sectionTitle ) );
        stepModel.setIsSectionTitle( true );
        getCurrentScenarioCase().addStep( stepModel );
    }

    @Override
    public void tagAdded( Class annotationClass, String... values ) {
        TagConfiguration tagConfig = toTagConfiguration( annotationClass );
        if( tagConfig == null ) {
            return;
        }

        List tags = toTags( tagConfig, Optional.empty() );
        if( tags.isEmpty() ) {
            return;
        }

        if( values.length > 0 ) {
            addTags( getExplodedTags( Iterables.getOnlyElement( tags ), values, null, tagConfig ) );
        } else {
            addTags( tags );
        }
    }

    public ReportModel getReportModel() {
        return reportModel;
    }

    public ScenarioModel getScenarioModel() {
        return scenarioModel;
    }

    public ScenarioCaseModel getScenarioCaseModel() {
        return scenarioCaseModel;
    }

}