Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow adding tags dynamically. #213

Merged
merged 3 commits into from
Jul 7, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
* Added a new comment() method to provide further information on specific step method invocations, see [#50](https://github.com/TNG/JGiven/issues/50).
* Steps can now have multiple attachments [#194](https://github.com/TNG/JGiven/issues/194).
* Tags can now be hidden from the navigation bar in the HTML report by setting the `showInNavigation` attribute to `false` [#211](https://github.com/TNG/JGiven/issues/211).
* Added a new CurrentScenario interface similar to CurrentStep.
* The CurrentScenario interface allows adding tags programmatically, see [#172](https://github.com/TNG/JGiven/issues/172).
* Allow tag annotations on step methods and step classes.

## Breaking Changes in the JSON model

Expand Down
23 changes: 23 additions & 0 deletions jgiven-core/src/main/java/com/tngtech/jgiven/CurrentScenario.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.tngtech.jgiven;

import java.lang.annotation.Annotation;

import com.tngtech.jgiven.annotation.ScenarioState;

/**
* This interface can be injected into a stage using the {@link ScenarioState} annotation.
* It provided programmatic access to the current scenario.
*
* @since 0.12.0
*/
public interface CurrentScenario {

/**
* Dynamically add a tag to the scenario.
* @param annotationClass The tag annotation class to use.
* @param values List of custom values.
* @since 0.12.0
*/
void addTag( Class<? extends Annotation> annotationClass, String... values );

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,28 @@
* Is used as an attribute of the {@link com.tngtech.jgiven.annotation.IsTag} annotation
* to dynamically generate a description for an annotation depending on its value.
* <p>
* Implementations of this interface must be a public non-abstract class that is not a non-static inner class
* Implementations of this interface must be a public non-abstract class that is not a non-static inner class
* and must have a public default constructor.
* </p>
*
*
* @since 0.6.3
*/
public interface TagDescriptionGenerator {

/**
* Implement this method to generate the description for the given annotation and its value.
* <p>
* Note that when the value of the annotation is an array and {@link com.tngtech.jgiven.annotation.IsTag#explodeArray()}
* Note that when the value of the annotation is an array and {@link com.tngtech.jgiven.annotation.IsTag#explodeArray()}
* is {@code true}, then this method is called for each value of the array and not once for the whole array.
* Otherwise it is called only once.
* </p>
* @param tagConfiguration the configuration of the tag. The values typically correspond to the {@link IsTag annotation}.
* @param tagConfiguration the configuration of the tag. The values typically correspond to the {@link IsTag annotation}.
* However, it is also possible to configure annotations to be tags using {@link com.tngtech.jgiven.annotation.JGivenConfiguration},
* in which case there is no {@link IsTag} annotation.
* @param annotation the actual annotation that was used as a tag
* @param annotation the actual annotation that was used as a tag. Note that this can be {@code null} in the case of
* dynamically added tags.
* @param value the value of the annotation. If the annotation has no value the default value is passed ({@link com.tngtech.jgiven.annotation.IsTag#value()}
*
*
* @return the description of the annotation for the given value
*/
String generateDescription( TagConfiguration tagConfiguration, Annotation annotation, Object value );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ public interface TagHrefGenerator {
* @param tagConfiguration the configuration of the tag. The values typically correspond to the {@link IsTag annotation}.
* However, it is also possible to configure annotations to be tags using {@link JGivenConfiguration},
* in which case there is no {@link IsTag} annotation.
* @param annotation the actual annotation that was used as a tag
* @param annotation the actual annotation that was used as a tag. Note that this can be {@code null} in the case of
* dynamically added tags.
* @param value the value of the annotation. If the annotation has no value the default value is passed ({@link IsTag#value()}
*
*
* @return the description of the annotation for the given value
*/
String generateHref( TagConfiguration tagConfiguration, Annotation annotation, Object value );
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tngtech.jgiven.impl;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.tngtech.jgiven.impl;

import com.google.common.base.CaseFormat;
import com.google.common.base.Optional;
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.*;
import com.tngtech.jgiven.attachment.Attachment;
Expand Down Expand Up @@ -172,6 +174,9 @@ public void stepMethodInvoked( Method method, List<NamedArgument> arguments, Inv
} else if( method.isAnnotationPresent( StepComment.class ) ) {
stepCommentAdded( arguments );
} else {
addTags( method.getAnnotations() );
addTags( method.getDeclaringClass().getAnnotations() );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried passing the receiver from cglib instead of using getDeclaringClass, but cglib subclasses the class to create the proxy which means any annotation not tagged with @Inherited gets lost. I'm not really sure if either way would be better than the other, so maybe this is completely fine…?


addStepMethod( method, arguments, mode, hasNestedSteps );
}
}
Expand Down Expand Up @@ -381,26 +386,35 @@ private void readAnnotations( Class<?> testClass, Method method ) {

public void addTags( Annotation... annotations ) {
for( Annotation annotation : annotations ) {
List<Tag> tags = toTags( annotation );
addTags( toTags( annotation ) );
}
}

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

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

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

public List<Tag> toTags( Annotation annotation ) {
Class<? extends Annotation> annotationType = annotation.annotationType();
IsTag isTag = annotationType.getAnnotation( IsTag.class );
TagConfiguration tagConfig;
if( isTag != null ) {
tagConfig = fromIsTag( isTag, annotation );
} else {
tagConfig = configuration.getTagConfiguration( annotationType );
}

TagConfiguration tagConfig = toTagConfiguration( annotationType );
if( tagConfig == null ) {
return Collections.emptyList();
}

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

private List<Tag> toTags( TagConfiguration tagConfig, Optional<Annotation> annotation ) {
Tag tag = new Tag( tagConfig.getAnnotationType() );

if( !Strings.isNullOrEmpty( tagConfig.getName() ) ) {
Expand Down Expand Up @@ -430,55 +444,74 @@ public List<Tag> toTags( Annotation annotation ) {
tag.setValue( tagConfig.getDefaultValue() );
}

if( tagConfig.isIgnoreValue() ) {
tag.setDescription( getDescriptionFromGenerator( tagConfig, annotation, value ) );
tag.setTags( tagConfig.getTags() );

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

return Arrays.asList( tag );
}

tag.setTags( tagConfig.getTags() );

try {
Method method = annotationType.getMethod( "value" );
value = method.invoke( annotation );
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<Tag> explodedTags = getExplodedTags( tag, objectArray, annotation, tagConfig );
List<Tag> 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, e );
log.error( "Error while getting 'value' method of annotation " + annotation.get(), e );
}

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

return Arrays.asList( tag );
}

public TagConfiguration fromIsTag( IsTag isTag, Annotation annotation ) {

String name = Strings.isNullOrEmpty( isTag.name() ) ? isTag.type() : isTag.name();

return TagConfiguration.builder( annotation.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, annotation ) )
.href( isTag.href() ).hrefGenerator( isTag.hrefGenerator() )
.showInNavigation( isTag.showInNavigation() ).build();
private TagConfiguration toTagConfiguration( Class<? extends Annotation> annotationType ) {
IsTag isTag = annotationType.getAnnotation( IsTag.class );
if( isTag != null ) {
return fromIsTag( isTag, annotationType );
}

return configuration.getTagConfiguration( annotationType );
}

private List<String> getTagNames( IsTag isTag, Annotation annotation ) {
List<Tag> tags = getTags( isTag, annotation );
public TagConfiguration fromIsTag( IsTag isTag, Class<? extends Annotation> annotationType ) {
String name = Strings.isNullOrEmpty( isTag.name() ) ? isTag.type() : 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<String> getTagNames( IsTag isTag, Class<? extends Annotation> annotationType ) {
List<Tag> tags = getTags( isTag, annotationType );
reportModel.addTags( tags );
List<String> tagNames = Lists.newArrayList();
for( Tag tag : tags ) {
Expand All @@ -487,10 +520,10 @@ private List<String> getTagNames( IsTag isTag, Annotation annotation ) {
return tagNames;
}

private List<Tag> getTags( IsTag isTag, Annotation annotation ) {
private List<Tag> getTags( IsTag isTag, Class<? extends Annotation> annotationType ) {
List<Tag> allTags = Lists.newArrayList();

for( Annotation a : annotation.annotationType().getAnnotations() ) {
for( Annotation a : annotationType.getAnnotations() ) {
if( a.annotationType().isAnnotationPresent( IsTag.class ) ) {
List<Tag> tags = toTags( a );
for( Tag tag : tags ) {
Expand Down Expand Up @@ -571,6 +604,25 @@ public void sectionAdded( String sectionTitle ) {
getCurrentScenarioCase().addStep( stepModel );
}

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

List<Tag> tags = toTags( tagConfig, Optional.<Annotation>absent() );
if( tags.isEmpty() ) {
return;
}

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

public ReportModel getReportModel() {
return reportModel;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.tngtech.jgiven.CurrentScenario;
import com.tngtech.jgiven.CurrentStep;
import com.tngtech.jgiven.annotation.AfterScenario;
import com.tngtech.jgiven.annotation.AfterStage;
Expand Down Expand Up @@ -80,6 +81,7 @@ public class StandaloneScenarioExecutor implements ScenarioExecutor {
public StandaloneScenarioExecutor() {
injector.injectValueByType( StandaloneScenarioExecutor.class, this );
injector.injectValueByType( CurrentStep.class, new StepAccessImpl() );
injector.injectValueByType( CurrentScenario.class, new ScenarioAccessImpl() );
}

protected static class StageState {
Expand All @@ -106,6 +108,15 @@ public void setExtendedDescription( String extendedDescription ) {
}
}

class ScenarioAccessImpl implements CurrentScenario {

@Override
public void addTag( Class<? extends Annotation> annotationClass, String... values ) {
listener.tagAdded( annotationClass, values );
}

}

class MethodHandler implements StepMethodHandler {
@Override
public void handleMethod( Object stageInstance, Method paramMethod, Object[] arguments, InvocationMode mode,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tngtech.jgiven.impl.intercept;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;

Expand Down Expand Up @@ -43,7 +44,8 @@ public void attachmentAdded( Attachment attachment ) {}
public void extendedDescriptionUpdated( String extendedDescription ) {}

@Override
public void sectionAdded( String sectionTitle ) {
public void sectionAdded( String sectionTitle ) {}

}
@Override
public void tagAdded( Class<? extends Annotation> annotationClass, String... values ) {}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.tngtech.jgiven.impl.intercept;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;

Expand Down Expand Up @@ -32,4 +33,6 @@ public interface ScenarioListener {
void extendedDescriptionUpdated( String extendedDescription );

void sectionAdded( String sectionTitle );

void tagAdded( Class<? extends Annotation> annotationClass, String... values );
}