diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc63574d05..5426c5f200 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,13 +2,28 @@
## New Features
+### Hierarchical Tags
+
+Tags can now have parent tags by tagging a tag annotation. This allows you to define tag hierarchies.
+
+#### Example
+
+The following example tags the `FeatureHtml5Report` annotation with the `FeatureReport` annotation:
+
+```
+@FeatureReport
+@IsTag( name = "HTML5 Report" )
+@Retention( RetentionPolicy.RUNTIME )
+public @interface FeatureHtml5Report { }
+```
+
### Enhanced Spring Support [#94](https://github.com/TNG/JGiven/pull/94)
* The Spring support has been greatly improved. JGiven Stages can now be directly managed by the Spring framework, resulting in a much better Spring integration.
** Note that the usage of Spring is optional and is provided by the `jgiven-spring` module.
* Introduced `@JGivenStage` to ease writing spring beans that act as JGiven stage
-### HTML5 Report
+### Hierarchical Package Structure in the HTML5 Report
* Classes are shown now in hierarchical navigation tree and scenarios can be listed by package [#91](https://github.com/TNG/JGiven/pull/91)
@@ -17,7 +32,7 @@
* HTML5 Report: tables with duplicate entries cannot be used as step parameters [#89](https://github.com/TNG/JGiven/issues/89)
* Fixed an issue that the `@Description` annotation was not regarded for methods with the `@IntroWord` [#87](https://github.com/TNG/JGiven/issues/87)
-## New Annotation
+## New Annotations
* Introduced the `@As` annotation that replaces the `@Description` annotation when used on step methods and test methods. The `@Description` annotation should only be used for descriptions of test classes.
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/IsTag.java b/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/IsTag.java
index 8c62474a4e..9c06eab4de 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/IsTag.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/IsTag.java
@@ -71,10 +71,21 @@
Class extends TagDescriptionGenerator> descriptionGenerator() default DefaultTagDescriptionGenerator.class;
/**
- * An optional type description that overrides the default which is the name of the annotation.
+ * @deprecated use {@link #name()} instead
*/
+ @Deprecated
String type() default "";
+ /**
+ * An optional name that overrides the default which is the name of the annotation.
+ *
+ * It is possible that multiple annotations have the same type name. However, in this case every
+ * annotation must have a specified value that must be unique.
+ *
+ * @since 0.7.4
+ */
+ String name() default "";
+
/**
* Whether the type should be prepended to the tag if the tag has a value.
*/
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/Pending.java b/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/Pending.java
new file mode 100644
index 0000000000..1ea996c7c0
--- /dev/null
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/annotation/Pending.java
@@ -0,0 +1,68 @@
+package com.tngtech.jgiven.annotation;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Marks methods of step definitions as not implemented yet.
+ * Such steps will not be executed, but will appear in
+ * the report as not implemented yet.
+ *
+ * This is useful if one already wants to define the scenario without
+ * already implementing all steps, for example, to verify that
+ * all acceptance criteria of a story are covered by the scenario.
+ *
+ * Annotating a stage class indicates
+ * that no step is implemented yet.
+ *
+ * Finally, a test method can be annotated to indicate that the whole
+ * test is not implemented yet. The test will then be ignored by the testing-framework.
+ * (In fact an AssumptionException is thrown. It depends on the test runner how this
+ * is interpreted)
+ * Currently only works for JUnit
+ *
+ *
+ *
+ */
+@Documented
+@Inherited
+@Retention( RUNTIME )
+@Target( { METHOD, TYPE } )
+@IsTag( ignoreValue = true, description = "Not implemented Scenarios" )
+public @interface Pending {
+ /**
+ * Optional description to describe when the implementation will be done.
+ */
+ String value() default "";
+
+ /**
+ * Instead of only reporting not implemented yet steps,
+ * the steps are actually executed.
+ * This is useful to see whether some steps fail, for example.
+ * Failing steps, however, have no influence on the overall test result.
+ */
+ boolean executeSteps() default false;
+
+ /**
+ * If no step fails during the execution of the test,
+ * the test will fail.
+ *
+ * This makes sense if one ensures that a not implemented feature
+ * always leads to failing tests in the spirit of test-driven development.
+ *
+ * If this is true, the executeSteps attribute is implicitly true.
+ */
+ boolean failIfPass() default false;
+}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/config/AbstractJGivenConfiguraton.java b/jgiven-core/src/main/java/com/tngtech/jgiven/config/AbstractJGivenConfiguraton.java
index 4622f69319..c61cca7324 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/config/AbstractJGivenConfiguraton.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/config/AbstractJGivenConfiguraton.java
@@ -9,7 +9,7 @@ public abstract class AbstractJGivenConfiguraton {
private final Map, TagConfiguration> tagConfigurations = Maps.newHashMap();
public final TagConfiguration.Builder configureTag( Class extends Annotation> tagAnnotation ) {
- TagConfiguration configuration = new TagConfiguration();
+ TagConfiguration configuration = new TagConfiguration( tagAnnotation );
tagConfigurations.put( tagAnnotation, configuration );
return new TagConfiguration.Builder( configuration );
}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/config/TagConfiguration.java b/jgiven-core/src/main/java/com/tngtech/jgiven/config/TagConfiguration.java
index ec4c58d18f..4845902eeb 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/config/TagConfiguration.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/config/TagConfiguration.java
@@ -1,7 +1,10 @@
package com.tngtech.jgiven.config;
+import java.lang.annotation.Annotation;
+import java.util.List;
+
+import com.google.common.collect.Lists;
import com.tngtech.jgiven.annotation.DefaultTagDescriptionGenerator;
-import com.tngtech.jgiven.annotation.IsTag;
import com.tngtech.jgiven.annotation.TagDescriptionGenerator;
/**
@@ -10,6 +13,7 @@
* @see com.tngtech.jgiven.annotation.IsTag for a documentation of the different values.
*/
public class TagConfiguration {
+ private final String annotationType;
private boolean ignoreValue;
private boolean explodeArray = true;
private boolean prependType;
@@ -18,7 +22,16 @@ public class TagConfiguration {
private String color = "";
private String cssClass = "";
private Class extends TagDescriptionGenerator> descriptionGenerator = DefaultTagDescriptionGenerator.class;
- private String type = "";
+ private String name = "";
+ private List tags = Lists.newArrayList();
+
+ public TagConfiguration( Class extends Annotation> tagAnnotation ) {
+ this.annotationType = tagAnnotation.getSimpleName();
+ }
+
+ public static Builder builder( Class extends Annotation> tagAnnotation ) {
+ return new Builder( new TagConfiguration( tagAnnotation ) );
+ }
public static class Builder {
final TagConfiguration configuration;
@@ -52,8 +65,17 @@ public Builder descriptionGenerator( Class extends TagDescriptionGenerator> de
return this;
}
+ /**
+ * @deprecated use {@link #name(String)} instead
+ */
+ @Deprecated
public Builder type( String s ) {
- configuration.type = s;
+ configuration.name = s;
+ return this;
+ }
+
+ public Builder name( String s ) {
+ configuration.name = s;
return this;
}
@@ -72,6 +94,15 @@ public Builder color( String color ) {
return this;
}
+ public Builder tags( List tags ) {
+ configuration.tags = tags;
+ return this;
+ }
+
+ public TagConfiguration build() {
+ return configuration;
+ }
+
}
/**
@@ -100,10 +131,20 @@ public Class extends TagDescriptionGenerator> getDescriptionGenerator() {
/**
* {@link com.tngtech.jgiven.annotation.IsTag#type()}
+ * @deprecated use {@link #getName()} instead
* @see com.tngtech.jgiven.annotation.IsTag
*/
+ @Deprecated
public String getType() {
- return type;
+ return name;
+ }
+
+ /**
+ * {@link com.tngtech.jgiven.annotation.IsTag#name()}
+ * @see com.tngtech.jgiven.annotation.IsTag
+ */
+ public String getName() {
+ return name;
}
/**
@@ -146,17 +187,12 @@ public String getCssClass() {
return cssClass;
}
- public static TagConfiguration fromIsTag( IsTag isTag ) {
- TagConfiguration result = new TagConfiguration();
- result.defaultValue = isTag.value();
- result.description = isTag.description();
- result.explodeArray = isTag.explodeArray();
- result.ignoreValue = isTag.ignoreValue();
- result.prependType = isTag.prependType();
- result.type = isTag.type();
- result.descriptionGenerator = isTag.descriptionGenerator();
- result.cssClass = isTag.cssClass();
- result.color = isTag.color();
- return result;
+ public List getTags() {
+ return tags;
}
+
+ public String getAnnotationType() {
+ return annotationType;
+ }
+
}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java
index e3f7d4ae9c..d161f7c9bf 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java
@@ -33,7 +33,7 @@ public void setModel( ReportModel scenarioCollectionModel ) {
}
public ReportModel getModel() {
- return modelBuilder.getScenarioCollectionModel();
+ return modelBuilder.getReportModel();
}
public T addStage( Class stepsClass ) {
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/DataTableScenarioHtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/DataTableScenarioHtmlWriter.java
index 10ef5dca11..bcd43d113b 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/DataTableScenarioHtmlWriter.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/DataTableScenarioHtmlWriter.java
@@ -2,15 +2,12 @@
import java.io.PrintWriter;
-import com.tngtech.jgiven.report.model.ScenarioCaseModel;
-import com.tngtech.jgiven.report.model.ScenarioModel;
-import com.tngtech.jgiven.report.model.StepModel;
-import com.tngtech.jgiven.report.model.Word;
+import com.tngtech.jgiven.report.model.*;
public class DataTableScenarioHtmlWriter extends ScenarioHtmlWriter {
- public DataTableScenarioHtmlWriter( PrintWriter writer ) {
- super( writer );
+ public DataTableScenarioHtmlWriter( PrintWriter writer, ReportModel reportModel ) {
+ super( writer, reportModel );
}
@Override
@@ -74,7 +71,7 @@ public void visit( StepModel stepModel ) {
}
@Override
- String formatValue(Word value) {
+ String formatValue( Word value ) {
String paramName = findParameterName( value );
return "<" + paramName + ">";
}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/MultiCaseScenarioHtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/MultiCaseScenarioHtmlWriter.java
index ce32011849..e691058982 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/MultiCaseScenarioHtmlWriter.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/MultiCaseScenarioHtmlWriter.java
@@ -2,10 +2,12 @@
import java.io.PrintWriter;
+import com.tngtech.jgiven.report.model.ReportModel;
+
public class MultiCaseScenarioHtmlWriter extends ScenarioHtmlWriter {
- public MultiCaseScenarioHtmlWriter( PrintWriter writer ) {
- super( writer );
+ public MultiCaseScenarioHtmlWriter( PrintWriter writer, ReportModel reportModel ) {
+ super( writer, reportModel );
}
}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ReportModelHtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ReportModelHtmlWriter.java
index 28b1d5c589..c242a51cfb 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ReportModelHtmlWriter.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ReportModelHtmlWriter.java
@@ -2,12 +2,7 @@
import static java.lang.String.format;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
import java.text.DateFormat;
import java.util.Date;
@@ -19,16 +14,13 @@
import com.tngtech.jgiven.impl.util.PrintWriterUtil;
import com.tngtech.jgiven.impl.util.ResourceUtil;
import com.tngtech.jgiven.impl.util.Version;
-import com.tngtech.jgiven.report.model.ReportModel;
-import com.tngtech.jgiven.report.model.ReportModelVisitor;
-import com.tngtech.jgiven.report.model.ReportStatistics;
-import com.tngtech.jgiven.report.model.ScenarioModel;
-import com.tngtech.jgiven.report.model.StatisticsCalculator;
+import com.tngtech.jgiven.report.model.*;
public class ReportModelHtmlWriter extends ReportModelVisitor {
protected final PrintWriter writer;
protected final HtmlWriterUtils utils;
private ReportStatistics statistics;
+ private ReportModel reportModel;
public ReportModelHtmlWriter( PrintWriter writer ) {
this.writer = writer;
@@ -61,7 +53,7 @@ private void closeDiv() {
}
public void write( ScenarioModel model ) {
- writeHtmlHeader(model.getClassName());
+ writeHtmlHeader( model.getClassName() );
model.accept( this );
writeHtmlFooter();
}
@@ -142,6 +134,7 @@ public static void writeToFile( File file, ReportModel model, HtmlTocWriter html
@Override
public void visit( ReportModel reportModel ) {
+ this.reportModel = reportModel;
writer.println( "
");
}
- private String diffClass( Word word ) {
+ private String diffClass(Word word) {
return word.isDifferent() ? " diff" : "";
}
- private void printArg( Word word ) {
- String value = word.getArgumentInfo().isParameter() ? formatValue( word ) : HtmlEscapers.htmlEscaper().escape(
- word.getFormattedValue() );
- printArgValue( word, value );
+ private void printArg(Word word) {
+ String value = word.getArgumentInfo().isParameter() ? formatValue(word) : HtmlEscapers.htmlEscaper().escape(
+ word.getFormattedValue());
+ printArgValue(word, value);
}
- private void printArgValue( Word word, String value ) {
- value = escapeToHtml( value );
- String multiLine = value.contains( " " ) ? " multiline" : "";
+ private void printArgValue(Word word, String value) {
+ value = escapeToHtml(value);
+ String multiLine = value.contains(" ") ? " multiline" : "";
String caseClass = word.getArgumentInfo().isParameter() ? "caseArgument" : "argument";
- writer.print( format( "%s", caseClass, multiLine, diffClass( word ), value ) );
+ writer.print(format("%s", caseClass, multiLine, diffClass(word), value));
}
- private String escapeToHtml( String value ) {
- return value.replaceAll( "(\r\n|\n)", " " );
+ private String escapeToHtml(String value) {
+ return value.replaceAll("(\r\n|\n)", " ");
}
- String formatValue( Word word ) {
- return HtmlEscapers.htmlEscaper().escape( word.getValue() );
+ String formatValue(Word word) {
+ return HtmlEscapers.htmlEscaper().escape(word.getValue());
}
}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/StaticHtmlReportGenerator.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/StaticHtmlReportGenerator.java
index e04f77824e..badd1ed425 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/StaticHtmlReportGenerator.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/StaticHtmlReportGenerator.java
@@ -69,11 +69,12 @@ public void writeEnd() {
}
- private void writeScenarios( HtmlTocWriter tocWriter, List failedScenarios, String name, String fileName ) {
- ReportModel completeReportModel = new ReportModel();
- completeReportModel.setScenarios( failedScenarios );
- completeReportModel.setClassName( name );
- ReportModelHtmlWriter.writeModelToFile( completeReportModel, tocWriter, new File( targetDirectory, fileName ) );
+ private void writeScenarios( HtmlTocWriter tocWriter, List scenarios, String name, String fileName ) {
+ ReportModel reportModel = new ReportModel();
+ reportModel.setScenarios( scenarios );
+ reportModel.setClassName( name );
+ reportModel.setTagMap( this.completeReportModel.getTagIdMap() );
+ ReportModelHtmlWriter.writeModelToFile( reportModel, tocWriter, new File( targetDirectory, fileName ) );
}
private void writeTagFiles( HtmlTocWriter tocWriter ) {
@@ -84,20 +85,21 @@ private void writeTagFiles( HtmlTocWriter tocWriter ) {
private void writeTagFile( Tag tag, List value, HtmlTocWriter tocWriter ) {
try {
- ReportModel completeReportModel = new ReportModel();
- completeReportModel.setClassName( tag.getName() );
+ ReportModel reportModel = new ReportModel();
+ reportModel.setClassName( tag.getName() );
if( tag.getValues().isEmpty() ) {
- completeReportModel.setClassName( completeReportModel.getClassName() + "." + tag.getValueString() );
+ reportModel.setClassName( reportModel.getClassName() + "." + tag.getValueString() );
}
- completeReportModel.setScenarios( value );
- completeReportModel.setDescription( tag.getDescription() );
+ reportModel.setScenarios( value );
+ reportModel.setDescription( tag.getDescription() );
+ reportModel.setTagMap( completeReportModel.getTagIdMap() );
String fileName = HtmlTocWriter.tagToFilename( tag );
File targetFile = new File( targetDirectory, fileName );
- ReportModelHtmlWriter.writeToFile( targetFile, completeReportModel, tocWriter );
+ ReportModelHtmlWriter.writeToFile( targetFile, reportModel, tocWriter );
} catch( Exception e ) {
- log.error( "Error while trying to write HTML file for tag " + tag.getName() );
+ log.error( "Error while trying to write HTML file for tag " + tag.getName(), e );
}
}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/CompleteReportModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/CompleteReportModel.java
index d4130eb1f8..89a4b76b32 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/CompleteReportModel.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/CompleteReportModel.java
@@ -19,16 +19,19 @@ public class CompleteReportModel {
protected final List failedScenarios = Lists.newArrayList();
protected final List pendingScenarios = Lists.newArrayList();
protected final List allScenarios = Lists.newArrayList();
+ protected final Map tagIdMap = Maps.newLinkedHashMap();
public void addModelFile( ReportModelFile modelFile ) {
ReportModel model = modelFile.model;
for( ScenarioModel scenario : model.getScenarios() ) {
- for( Tag tag : scenario.getTags() ) {
+ for( String tagId : scenario.getTagIds() ) {
+ Tag tag = model.getTagWithId( tagId );
addToMap( tag, scenario );
}
}
+ tagIdMap.putAll( model.getTagMap() );
ReportStatistics statistics = new StatisticsCalculator().getStatistics( model );
statisticsMap.put( modelFile, statistics );
@@ -82,4 +85,8 @@ public List getScenariosByTag( Tag tag ) {
public List getAllReportModels() {
return models;
}
+
+ public Map getTagIdMap() {
+ return tagIdMap;
+ }
}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModel.java
index e767baad38..63a7e96f7c 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModel.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModel.java
@@ -1,14 +1,13 @@
package com.tngtech.jgiven.report.model;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.EnumSet;
-import java.util.List;
+import java.util.*;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.tngtech.jgiven.impl.util.AssertionUtil;
public class ReportModel {
/**
@@ -23,57 +22,59 @@ public class ReportModel {
private List scenarios = Lists.newArrayList();
- public void accept( ReportModelVisitor visitor ) {
- visitor.visit( this );
+ private Map tagMap = Maps.newLinkedHashMap();
+
+ public void accept(ReportModelVisitor visitor) {
+ visitor.visit(this);
List sorted = sortByDescription();
- for( ScenarioModel m : sorted ) {
- m.accept( visitor );
+ for (ScenarioModel m : sorted) {
+ m.accept(visitor);
}
- visitor.visitEnd( this );
+ visitor.visitEnd(this);
}
private List sortByDescription() {
- List sorted = Lists.newArrayList( getScenarios() );
- Collections.sort( sorted, new Comparator() {
+ List sorted = Lists.newArrayList(getScenarios());
+ Collections.sort(sorted, new Comparator() {
@Override
- public int compare( ScenarioModel o1, ScenarioModel o2 ) {
- return o1.getDescription().toLowerCase().compareTo( o2.getDescription().toLowerCase() );
+ public int compare(ScenarioModel o1, ScenarioModel o2) {
+ return o1.getDescription().toLowerCase().compareTo(o2.getDescription().toLowerCase());
}
- } );
+ });
return sorted;
}
public ScenarioModel getLastScenarioModel() {
- return getScenarios().get( getScenarios().size() - 1 );
+ return getScenarios().get(getScenarios().size() - 1);
}
- public Optional findScenarioModel( String scenarioDescription ) {
- for( ScenarioModel model : getScenarios() ) {
- if( model.getDescription().equals( scenarioDescription ) ) {
- return Optional.of( model );
+ public Optional findScenarioModel(String scenarioDescription) {
+ for (ScenarioModel model : getScenarios()) {
+ if (model.getDescription().equals(scenarioDescription)) {
+ return Optional.of(model);
}
}
return Optional.absent();
}
public StepModel getFirstStepModelOfLastScenario() {
- return getLastScenarioModel().getCase( 0 ).getStep( 0 );
+ return getLastScenarioModel().getCase(0).getStep(0);
}
- public void addScenarioModel( ScenarioModel currentScenarioModel ) {
- getScenarios().add( currentScenarioModel );
+ public void addScenarioModel(ScenarioModel currentScenarioModel) {
+ getScenarios().add(currentScenarioModel);
}
public String getSimpleClassName() {
- return Iterables.getLast( Splitter.on( '.' ).split( getClassName() ) );
+ return Iterables.getLast(Splitter.on('.').split(getClassName()));
}
public String getDescription() {
return description;
}
- public void setDescription( String description ) {
+ public void setDescription(String description) {
this.description = description;
}
@@ -81,7 +82,7 @@ public String getClassName() {
return className;
}
- public void setClassName( String className ) {
+ public void setClassName(String className) {
this.className = className;
}
@@ -89,36 +90,59 @@ public List getScenarios() {
return scenarios;
}
- public void setScenarios( List scenarios ) {
+ public void setScenarios(List scenarios) {
this.scenarios = scenarios;
}
public String getPackageName() {
- int index = this.className.lastIndexOf( '.' );
- if( index == -1 ) {
+ int index = this.className.lastIndexOf('.');
+ if (index == -1) {
return "";
}
- return this.className.substring( 0, index );
+ return this.className.substring(0, index);
}
public List getFailedScenarios() {
- return getScenariosWithStatus( ExecutionStatus.FAILED );
+ return getScenariosWithStatus(ExecutionStatus.FAILED);
}
public List getPendingScenarios() {
- return getScenariosWithStatus( ExecutionStatus.NONE_IMPLEMENTED, ExecutionStatus.PARTIALLY_IMPLEMENTED );
+ return getScenariosWithStatus(ExecutionStatus.NONE_IMPLEMENTED, ExecutionStatus.PARTIALLY_IMPLEMENTED);
}
- public List getScenariosWithStatus( ExecutionStatus first, ExecutionStatus... rest ) {
- EnumSet stati = EnumSet.of( first, rest );
+ public List getScenariosWithStatus(ExecutionStatus first, ExecutionStatus... rest) {
+ EnumSet stati = EnumSet.of(first, rest);
List result = Lists.newArrayList();
- for( ScenarioModel m : scenarios ) {
+ for (ScenarioModel m : scenarios) {
ExecutionStatus executionStatus = m.getExecutionStatus();
- if( stati.contains( executionStatus ) ) {
- result.add( m );
+ if (stati.contains(executionStatus)) {
+ result.add(m);
}
}
return result;
}
+ public void addTag(Tag tag) {
+ this.tagMap.put(tag.toIdString(), tag);
+ }
+
+ public void addTags(List tags) {
+ for (Tag tag : tags) {
+ addTag(tag);
+ }
+ }
+
+ public Tag getTagWithId(String tagId) {
+ Tag tag = this.tagMap.get(tagId);
+ AssertionUtil.assertNotNull(tag, "Could not find tag with id " + tagId);
+ return tag;
+ }
+
+ public Map getTagMap() {
+ return tagMap;
+ }
+
+ public void setTagMap(Map tagMap) {
+ this.tagMap = tagMap;
+ }
}
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java
index 5380c989bb..f10df9eb1c 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ReportModelBuilder.java
@@ -255,7 +255,7 @@ private static String nameWithoutUnderlines( Method paramMethod ) {
return paramMethod.getName().replace( '_', ' ' );
}
- public ReportModel getScenarioCollectionModel() {
+ public ReportModel getReportModel() {
return reportModel;
}
@@ -340,9 +340,11 @@ private void readAnnotations( Method method ) {
}
}
- private void addTags( Annotation[] annotations ) {
+ public void addTags( Annotation... annotations ) {
for( Annotation annotation : annotations ) {
- this.currentScenarioModel.addTags( toTags( annotation ) );
+ List tags = toTags( annotation );
+ this.reportModel.addTags( tags );
+ this.currentScenarioModel.addTags( tags );
}
}
@@ -351,7 +353,7 @@ public List toTags( Annotation annotation ) {
IsTag isTag = annotationType.getAnnotation( IsTag.class );
TagConfiguration tagConfig;
if( isTag != null ) {
- tagConfig = TagConfiguration.fromIsTag( isTag );
+ tagConfig = fromIsTag( isTag, annotation );
} else {
tagConfig = configuration.getTagConfiguration( annotationType );
}
@@ -360,14 +362,12 @@ public List toTags( Annotation annotation ) {
return Collections.emptyList();
}
- String type = annotationType.getSimpleName();
+ Tag tag = new Tag( tagConfig.getAnnotationType() );
- if( !Strings.isNullOrEmpty( tagConfig.getType() ) ) {
- type = tagConfig.getType();
+ if( !Strings.isNullOrEmpty( tagConfig.getName() ) ) {
+ tag.setName( tagConfig.getName() );
}
- Tag tag = new Tag( type );
-
if( tagConfig.isPrependType() ) {
tag.setPrependType( true );
}
@@ -390,6 +390,8 @@ public List toTags( Annotation annotation ) {
return Arrays.asList( tag );
}
+ tag.setTags( tagConfig.getTags() );
+
try {
Method method = annotationType.getMethod( "value" );
value = method.invoke( annotation );
@@ -397,7 +399,8 @@ public List toTags( Annotation annotation ) {
if( value.getClass().isArray() ) {
Object[] objectArray = (Object[]) value;
if( tagConfig.isExplodeArray() ) {
- return getExplodedTags( tag, objectArray, annotation, tagConfig );
+ List explodedTags = getExplodedTags( tag, objectArray, annotation, tagConfig );
+ return explodedTags;
}
tag.setValue( toStringList( objectArray ) );
@@ -415,6 +418,50 @@ public List toTags( Annotation annotation ) {
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() )
+ .tags( getTagNames( isTag, annotation ) )
+ .build();
+
+ }
+
+ private List getTagNames( IsTag isTag, Annotation annotation ) {
+ List tags = getTags( isTag, annotation );
+ reportModel.addTags( tags );
+ List tagNames = Lists.newArrayList();
+ for( Tag tag : tags ) {
+ tagNames.add( tag.toIdString() );
+ }
+ return tagNames;
+ }
+
+ private List getTags( IsTag isTag, Annotation annotation ) {
+ List allTags = Lists.newArrayList();
+
+ for( Annotation a : annotation.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();
@@ -436,12 +483,9 @@ private String getDescriptionFromGenerator( TagConfiguration tagConfiguration, A
private List getExplodedTags( Tag originalTag, Object[] values, Annotation annotation, TagConfiguration tagConfig ) {
List result = Lists.newArrayList();
for( Object singleValue : values ) {
- Tag newTag = new Tag( originalTag.getName(), String.valueOf( singleValue ) );
- newTag.setDescription( originalTag.getDescription() );
- newTag.setPrependType( originalTag.isPrependType() );
+ Tag newTag = originalTag.copy();
+ newTag.setValue( String.valueOf( singleValue ) );
newTag.setDescription( getDescriptionFromGenerator( tagConfig, annotation, singleValue ) );
- newTag.setColor( originalTag.getColor() );
- newTag.setCssClass( originalTag.getCssClass() );
result.add( newTag );
}
return result;
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioModel.java
index 4d93e42758..29005ae76a 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioModel.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioModel.java
@@ -12,7 +12,11 @@ public class ScenarioModel {
private String className;
private String testMethodName;
private String description;
- private Set tags = Sets.newLinkedHashSet();
+
+ /**
+ * A list of tag ids
+ */
+ private Set tagIds = Sets.newLinkedHashSet();
private boolean notImplementedYet;
private List explicitParameters = Lists.newArrayList();
private List derivedParameters = Lists.newArrayList();
@@ -30,7 +34,7 @@ public void accept( ReportModelVisitor visitor ) {
}
public void addCase( ScenarioCaseModel scenarioCase ) {
- scenarioCase.setCaseNr(scenarioCases.size() + 1);
+ scenarioCase.setCaseNr( scenarioCases.size() + 1 );
scenarioCases.add( scenarioCase );
}
@@ -48,11 +52,13 @@ public ScenarioCaseModel getCase( int i ) {
}
public void addTag( Tag tag ) {
- tags.add( tag );
+ tagIds.add(tag.toIdString());
}
public void addTags( List tags ) {
- this.tags.addAll( tags );
+ for( Tag tag : tags ) {
+ addTag( tag );
+ }
}
public void addParameterNames( String... params ) {
@@ -72,8 +78,8 @@ public List getScenarioCases() {
return scenarioCases;
}
- public List getTags() {
- return Lists.newArrayList( tags );
+ public List getTagIds() {
+ return Lists.newArrayList(tagIds);
}
public boolean isCasesAsTable() {
@@ -132,8 +138,8 @@ public void setDescription( String description ) {
this.description = description;
}
- public void setTags( Set tags ) {
- this.tags = tags;
+ public void setTagIds(Set tagIds) {
+ this.tagIds = tagIds;
}
public boolean isNotImplementedYet() {
diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/Tag.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/Tag.java
index 2ee4b18a32..badac1b20e 100644
--- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/Tag.java
+++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/Tag.java
@@ -12,9 +12,14 @@
*/
public class Tag {
/**
- * The name/type of this tag
+ * The type of the annotation of the tag
*/
- private final String name;
+ private final String type;
+
+ /**
+ * An optional name of the tag. If not set, the type is the name
+ */
+ private String name;
/**
* An optional value
@@ -44,16 +49,31 @@ public class Tag {
*/
private String cssClass;
- public Tag( String name ) {
- this.name = name;
+ /**
+ * An optional (maybe null) list of tags that this tag is tagged with.
+ * The tags are normalized as follows: [-value].
+ */
+ private List tags;
+
+ public Tag( String type ) {
+ this.type = type;
}
- public Tag( String name, Object value ) {
- this.name = name;
+ public Tag( String type, Object value ) {
+ this( type );
this.value = value;
}
+ public Tag( String type, String name, Object value ) {
+ this( type, value );
+ this.name = name;
+ }
+
public String getName() {
+ if( name == null ) {
+ return type;
+ }
+
return name;
}
@@ -132,9 +152,16 @@ public String getValueString() {
return Joiner.on( ", " ).join( getValues() );
}
+ public String toIdString() {
+ if( value != null ) {
+ return type + "-" + getValueString();
+ }
+ return type;
+ }
+
@Override
public int hashCode() {
- return Objects.hashCode( getName(), value );
+ return Objects.hashCode( getType(), getName(), value );
}
@Override
@@ -149,7 +176,8 @@ public boolean equals( Object obj ) {
return false;
}
Tag other = (Tag) obj;
- return Objects.equal( getName(), other.getName() )
+ return Objects.equal( getType(), other.getType() )
+ && Objects.equal( getName(), other.getName() )
&& Objects.equal( value, other.value );
}
@@ -169,4 +197,36 @@ public String toEscapedString() {
static String escape( String string ) {
return string.replaceAll( "[^\\p{Alnum}-]", "_" );
}
+
+ public void setName( String name ) {
+ this.name = name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public List getTags() {
+ if( tags == null ) {
+ return Collections.emptyList();
+ }
+ return tags;
+ }
+
+ public void setTags( List tags ) {
+ if( tags != null && !tags.isEmpty() ) {
+ this.tags = tags;
+ }
+ }
+
+ public Tag copy() {
+ Tag tag = new Tag( type, name, value );
+ tag.cssClass = this.cssClass;
+ tag.color = this.color;
+ tag.description = this.description;
+ tag.prependType = this.prependType;
+ tag.tags = this.tags;
+ return tag;
+ }
+
}
diff --git a/jgiven-core/src/test/java/com/tngtech/jgiven/report/model/ReportModelBuilderTest.java b/jgiven-core/src/test/java/com/tngtech/jgiven/report/model/ReportModelBuilderTest.java
index 5990aa0caa..0696a7c154 100644
--- a/jgiven-core/src/test/java/com/tngtech/jgiven/report/model/ReportModelBuilderTest.java
+++ b/jgiven-core/src/test/java/com/tngtech/jgiven/report/model/ReportModelBuilderTest.java
@@ -25,6 +25,8 @@
@RunWith( DataProviderRunner.class )
public class ReportModelBuilderTest extends ScenarioTestBase {
+ private ReportModelBuilder reportModelBuilder;
+
@DataProvider
public static Object[][] testData() {
return new Object[][] {
@@ -108,6 +110,44 @@ public void testAnnotationWithValueParsing() throws Exception {
assertThat( tags.get( 0 ).getValues() ).containsExactly( "testvalue" );
}
+ @IsTag( name = "AnotherName" )
+ @Retention( RetentionPolicy.RUNTIME )
+ @interface AnnotationWithName {}
+
+ @AnnotationWithName( )
+ static class AnnotationWithNameTestClass {}
+
+ @Test
+ public void testAnnotationWithName() throws Exception {
+ ReportModelBuilder modelBuilder = new ReportModelBuilder();
+ List tags = modelBuilder.toTags( AnnotationWithNameTestClass.class.getAnnotations()[0] );
+ assertThat( tags ).hasSize( 1 );
+ Tag tag = tags.get( 0 );
+ assertThat( tag.getName() ).isEqualTo( "AnotherName" );
+ assertThat( tag.getValues() ).isEmpty();
+ assertThat( tag.toIdString() ).isEqualTo( "AnnotationWithName" );
+ }
+
+ @IsTag( ignoreValue = true )
+ @Retention( RetentionPolicy.RUNTIME )
+ @interface AnnotationWithIgnoredValue {
+ String value();
+ }
+
+ @AnnotationWithIgnoredValue( "testvalue" )
+ static class AnnotationWithIgnoredValueTestClass {}
+
+ @Test
+ public void testAnnotationWithIgnoredValueParsing() throws Exception {
+ ReportModelBuilder modelBuilder = new ReportModelBuilder();
+ List tags = modelBuilder.toTags( AnnotationWithIgnoredValueTestClass.class.getAnnotations()[0] );
+ assertThat( tags ).hasSize( 1 );
+ Tag tag = tags.get( 0 );
+ assertThat( tag.getName() ).isEqualTo( "AnnotationWithIgnoredValue" );
+ assertThat( tag.getValues() ).isEmpty();
+ assertThat( tag.toIdString() ).isEqualTo( "AnnotationWithIgnoredValue" );
+ }
+
@IsTag
@Retention( RetentionPolicy.RUNTIME )
@interface AnnotationWithArray {
@@ -233,4 +273,33 @@ public void abstract_steps_should_appear_in_the_report_model() throws Throwable
StepModel step = getScenario().getModel().getFirstStepModelOfLastScenario();
assertThat( step.words.get( 0 ).getFormattedValue() ).isEqualTo( "abstract step" );
}
+
+ @IsTag
+ @Retention( RetentionPolicy.RUNTIME )
+ @interface ParentTag {}
+
+ @IsTag
+ @Retention( RetentionPolicy.RUNTIME )
+ @interface ParentTagWithValue {
+ String value();
+ }
+
+ @ParentTagWithValue( "SomeValue" )
+ @ParentTag
+ @IsTag
+ @Retention( RetentionPolicy.RUNTIME )
+ @interface TagWithParentTags {}
+
+ @TagWithParentTags
+ static class AnnotationWithParentTag {}
+
+ @Test
+ public void testAnnotationWithParentTag() throws Exception {
+ reportModelBuilder = new ReportModelBuilder();
+ List tags = reportModelBuilder.toTags( AnnotationWithParentTag.class.getAnnotations()[0] );
+ assertThat( tags ).hasSize( 1 );
+ assertThat( tags.get( 0 ).getTags() ).containsAll( Arrays.asList(
+ "ParentTag", "ParentTagWithValue-SomeValue" ) );
+ }
+
}
diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/coffeemachine/ServeCoffeeTest.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/coffeemachine/ServeCoffeeTest.java
index bd6b62eb98..1af07c7a96 100644
--- a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/coffeemachine/ServeCoffeeTest.java
+++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/coffeemachine/ServeCoffeeTest.java
@@ -6,7 +6,7 @@
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.jgiven.annotation.Description;
-import com.tngtech.jgiven.examples.annotation.Order;
+import com.tngtech.jgiven.examples.tags.Order;
import com.tngtech.jgiven.examples.coffeemachine.steps.GivenCoffee;
import com.tngtech.jgiven.examples.coffeemachine.steps.ThenCoffee;
import com.tngtech.jgiven.examples.coffeemachine.steps.WhenCoffee;
diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/AnotherExampleSubCategory.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/AnotherExampleSubCategory.java
new file mode 100644
index 0000000000..fd8cb6c768
--- /dev/null
+++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/AnotherExampleSubCategory.java
@@ -0,0 +1,15 @@
+package com.tngtech.jgiven.examples.tags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import com.tngtech.jgiven.annotation.IsTag;
+
+/**
+ * Defines a tag that
+ */
+@ExampleCategory
+@CategoryWithValue( "Another Category" )
+@IsTag
+@Retention( RetentionPolicy.RUNTIME )
+public @interface AnotherExampleSubCategory {}
diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/CategoryWithValue.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/CategoryWithValue.java
new file mode 100644
index 0000000000..89ace1cedf
--- /dev/null
+++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/CategoryWithValue.java
@@ -0,0 +1,15 @@
+package com.tngtech.jgiven.examples.tags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import com.tngtech.jgiven.annotation.IsTag;
+
+/**
+ * Demonstrates that category tags can have values
+ */
+@IsTag( prependType = false )
+@Retention( RetentionPolicy.RUNTIME )
+public @interface CategoryWithValue {
+ String value();
+}
diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleCategory.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleCategory.java
new file mode 100644
index 0000000000..f0a2332779
--- /dev/null
+++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleCategory.java
@@ -0,0 +1,13 @@
+package com.tngtech.jgiven.examples.tags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import com.tngtech.jgiven.annotation.IsTag;
+
+/**
+ * Defines a tag that is used as a category
+ */
+@IsTag
+@Retention( RetentionPolicy.RUNTIME )
+public @interface ExampleCategory {}
diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleSubCategory.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleSubCategory.java
new file mode 100644
index 0000000000..079a0d8fa6
--- /dev/null
+++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/ExampleSubCategory.java
@@ -0,0 +1,15 @@
+package com.tngtech.jgiven.examples.tags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import com.tngtech.jgiven.annotation.IsTag;
+
+/**
+ * Defines a tag that
+ */
+@ExampleCategory
+@CategoryWithValue( "Some Category" )
+@IsTag
+@Retention( RetentionPolicy.RUNTIME )
+public @interface ExampleSubCategory {}
diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/annotation/Order.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/Order.java
similarity index 86%
rename from jgiven-examples/src/test/java/com/tngtech/jgiven/examples/annotation/Order.java
rename to jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/Order.java
index 011c64ca41..88125ebc31 100644
--- a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/annotation/Order.java
+++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/Order.java
@@ -1,4 +1,4 @@
-package com.tngtech.jgiven.examples.annotation;
+package com.tngtech.jgiven.examples.tags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
diff --git a/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/TagHierarchyExampleTest.java b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/TagHierarchyExampleTest.java
new file mode 100644
index 0000000000..3c7cb3e73e
--- /dev/null
+++ b/jgiven-examples/src/test/java/com/tngtech/jgiven/examples/tags/TagHierarchyExampleTest.java
@@ -0,0 +1,46 @@
+package com.tngtech.jgiven.examples.tags;
+
+import com.tngtech.jgiven.junit.SimpleScenarioTest;
+import org.junit.Test;
+
+/**
+ * This example shows how hierarchical tags work.
+ *
+ * Hierarchical tags can be created by just annotating a tag annotation with another tag.
+ * The other tag becomes a parent tag or category tag. This makes it possible to structure
+ * tags hierarchically. It is even possible to form multiple hierarchies/categories so that a
+ * tag is contained multiple categories.
+ *