Skip to content

Commit

Permalink
Allow adding tags dynamically.
Browse files Browse the repository at this point in the history
This commit allows adding tags dynamically to a scenario at runtime.
Furthermore, it implicitly fixes an issue where nested tags, description
and href would not be evaluated if ignoreValue was set to true.

fixes TNG#172
  • Loading branch information
Airblader committed Jul 2, 2016
1 parent be90df1 commit d06fb42
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* 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 addTag() to ScenarioBase to allow adding tags dynamically, see [#172](https://github.com/TNG/JGiven/issues/172).

## Breaking Changes in the JSON model

Expand Down
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 Expand Up @@ -114,4 +115,14 @@ public void section( String sectionTitle ) {
executor.addSection( sectionTitle );
}

/**
* Dynamically add a tag to the scenario.
* @param annotationClass The tag annotation class to use.
* @param values List of custom values. This must be supported by the given tag.
* @since 0.12.0
*/
public void addTag( Class<? extends Annotation> annotationClass, String... values ) {
executor.addTag( annotationClass, values );
}

}
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 All @@ -25,6 +26,8 @@ public enum State {

void addSection( String sectionTitle );

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

void readScenarioState( Object object );

/**
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 @@ -381,26 +383,26 @@ private void readAnnotations( Class<?> testClass, Method method ) {

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

private void addTags( List<Tag> tags ) {
this.reportModel.addTags( tags );
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 +432,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 +508,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 +592,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 @@ -533,4 +533,9 @@ public void addSection( String sectionTitle ) {
listener.sectionAdded( sectionTitle );
}

@Override
public void addTag( Class<? extends Annotation> annotationClass, String... values ) {
listener.tagAdded( annotationClass, 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 @@ -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 );
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import org.junit.Test;
Expand Down Expand Up @@ -156,6 +157,25 @@ public void testAnnotationWithDescription() throws Exception {
assertThat( tags.get( 0 ).getDescription() ).isEqualTo( "Some Description" );
}

@IsTag( description = "Some Description", ignoreValue = true )
@Retention( RetentionPolicy.RUNTIME )
@TagWithDescription
@interface TagWithDescriptionAndIgnoreValue {
String value();
}

@TagWithDescriptionAndIgnoreValue( value = "some value" )
static class AnnotationWithDescriptionAndIgnoreValue {}

@Test
public void testAnnotationWithDescriptionAndIgnoreValue() throws Exception {
List<Tag> tags = getScenarioModelBuilder().toTags( AnnotationWithDescriptionAndIgnoreValue.class.getAnnotations()[0] );
assertThat( tags ).hasSize( 1 );
assertThat( tags.get( 0 ).getValues() ).isEmpty();
assertThat( tags.get( 0 ).getDescription() ).isEqualTo( "Some Description" );
assertThat( tags.get( 0 ).getTags() ).hasSize( 1 );
}

@IsTag
@Retention( RetentionPolicy.RUNTIME )
@interface ParentTag {}
Expand Down Expand Up @@ -183,6 +203,27 @@ public void testAnnotationWithParentTag() throws Exception {
"ParentTag", "ParentTagWithValue-SomeValue" ) );
}

@IsTag( value = "default" )
@Retention( RetentionPolicy.RUNTIME )
@interface DynamicTag {}

@Test
public void testAddTagDynamically() throws Exception {
ReportModel reportModel = new ReportModel();
ScenarioModelBuilder scenarioModelBuilder = getScenarioModelBuilder();
scenarioModelBuilder.setReportModel( reportModel );
scenarioModelBuilder.scenarioStarted( "Test" );

scenarioModelBuilder.tagAdded( DynamicTag.class, "A", "B" );
scenarioModelBuilder.tagAdded( DynamicTag.class );
assertThat( reportModel.getTagMap() ).hasSize( 3 );

Iterator<Tag> iterator = reportModel.getTagMap().values().iterator();
assertThat( iterator.next().getValues() ).containsExactly( "A" );
assertThat( iterator.next().getValues() ).containsExactly( "B" );
assertThat( iterator.next().getValues() ).containsExactly( "default" );
}

@DataProvider
public static Object[][] testData() {
return new Object[][] {
Expand Down
Loading

0 comments on commit d06fb42

Please sign in to comment.