From f0c8fd54990ff776db75c768c37335450de3a281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4fer?= Date: Sun, 7 Sep 2014 14:40:36 +0200 Subject: [PATCH] Measure duration of steps, cases, and scenarios (Issue-#13) --- .../JGivenInternalDefectException.java | 14 +++++ .../tngtech/jgiven/impl/ScenarioExecutor.java | 16 +++++- .../impl/intercept/NoOpScenarioListener.java | 6 +++ .../impl/intercept/ScenarioListener.java | 4 ++ .../impl/intercept/StepMethodHandler.java | 5 +- .../impl/intercept/StepMethodInterceptor.java | 14 +++-- .../jgiven/impl/util/AssertionUtil.java | 27 ++++++++++ .../html/DataTableScenarioHtmlWriter.java | 8 +-- .../jgiven/report/html/HtmlWriter.java | 24 +++++---- .../report/html/ScenarioHtmlWriter.java | 53 +++++++++++++++---- .../html/StaticHtmlReportGenerator.java | 12 ++--- .../report/impl/CaseArgumentAnalyser.java | 2 +- .../report/impl/CommonReportHelper.java | 2 +- .../jgiven/report/model/ReportModel.java | 38 ++++++++++--- .../report/model/ReportModelBuilder.java | 29 ++++++++-- .../report/model/ScenarioCaseModel.java | 13 +++++ .../jgiven/report/model/ScenarioModel.java | 13 +++++ .../jgiven/report/model/StepModel.java | 13 +++++ .../jgiven/report/text/PlainTextReporter.java | 2 +- .../tngtech/jgiven/report/html/default.css | 11 ++-- .../java/com/tngtech/jgiven/ThenTestStep.java | 3 +- .../jgiven/report/html/HtmlWriterTest.java | 3 +- .../jgiven/junit/DataProviderTest.java | 4 +- .../jgiven/testng/ScenarioTestListener.java | 2 +- .../report/html/HtmlWriterScenarioTest.java | 36 ++++++++++--- .../jgiven/report/html/ThenHtmlOutput.java | 14 +++-- .../html/ThenStaticHtmlReportGenerator.java | 2 +- .../jgiven/report/json/GivenJsonReports.java | 2 +- .../jgiven/report/model/GivenReportModel.java | 24 ++++++--- .../text/ThenPlainTextReportGenerator.java | 2 +- .../tngtech/jgiven/tags/FeatureDuration.java | 12 +++++ 31 files changed, 327 insertions(+), 83 deletions(-) create mode 100644 jgiven-core/src/main/java/com/tngtech/jgiven/exception/JGivenInternalDefectException.java create mode 100644 jgiven-core/src/main/java/com/tngtech/jgiven/impl/util/AssertionUtil.java create mode 100644 jgiven-tests/src/test/java/com/tngtech/jgiven/tags/FeatureDuration.java diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/exception/JGivenInternalDefectException.java b/jgiven-core/src/main/java/com/tngtech/jgiven/exception/JGivenInternalDefectException.java new file mode 100644 index 0000000000..b97d12ba38 --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/exception/JGivenInternalDefectException.java @@ -0,0 +1,14 @@ +package com.tngtech.jgiven.exception; + +/** + * If this exception is thrown there is most likely a bug in JGiven. + */ +public class JGivenInternalDefectException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public JGivenInternalDefectException( String msg ) { + super( msg + + ". This is most propably due to an internal defect in JGiven and was not your fault. " + + "Please consider writing a bug report on github.com/TNG/JGiven" ); + } +} diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java index 57eac707b5..005b2c76d9 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java @@ -94,7 +94,8 @@ static class StageState { class MethodHandler implements StepMethodHandler { @Override - public void handleMethod( Object stageInstance, Method paramMethod, Object[] arguments, InvocationMode mode ) throws Throwable { + public void handleMethod( Object stageInstance, Method paramMethod, Object[] arguments, InvocationMode mode ) + throws Throwable { if( paramMethod.isSynthetic() ) { return; } @@ -122,6 +123,11 @@ public void handleThrowable( Throwable t ) throws Throwable { failed( t ); } + @Override + public void handleMethodFinished( long durationInNanos ) { + listener.stepMethodFinished( durationInNanos ); + } + } @SuppressWarnings( "unchecked" ) @@ -289,6 +295,14 @@ public void finished() throws Throwable { state = FINISHED; methodInterceptor.enableMethodHandling( false ); + try { + callFinishLifeCycleMethods(); + } finally { + listener.scenarioFinished(); + } + } + + private void callFinishLifeCycleMethods() throws Throwable { Throwable firstThrownException = failedException; if( beforeStepsWereExecuted ) { if( currentStage != null ) { diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/NoOpScenarioListener.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/NoOpScenarioListener.java index ed57ae33f4..fd5250b5d5 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/NoOpScenarioListener.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/NoOpScenarioListener.java @@ -24,4 +24,10 @@ public void introWordAdded( String word ) {} @Override public void stepMethodFailed( Throwable t ) {} + + @Override + public void stepMethodFinished( long durationInNanos ) {} + + @Override + public void scenarioFinished() {} } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/ScenarioListener.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/ScenarioListener.java index aceb55fa6f..b083652ccd 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/ScenarioListener.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/ScenarioListener.java @@ -19,4 +19,8 @@ public interface ScenarioListener { void stepMethodFailed( Throwable t ); + void stepMethodFinished( long durationInNanos ); + + void scenarioFinished(); + } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodHandler.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodHandler.java index 7dfba76bf9..0ab17074c0 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodHandler.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodHandler.java @@ -3,7 +3,10 @@ import java.lang.reflect.Method; public interface StepMethodHandler { - void handleMethod( Object targetObject, Method paramMethod, Object[] arguments, InvocationMode mode ) throws Throwable; + void handleMethod( Object targetObject, Method paramMethod, Object[] arguments, InvocationMode mode ) + throws Throwable; void handleThrowable( Throwable t ) throws Throwable; + + void handleMethodFinished( long durationInNanos ); } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodInterceptor.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodInterceptor.java index 1b1dd27482..246a286e56 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodInterceptor.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodInterceptor.java @@ -30,10 +30,11 @@ public StepMethodInterceptor( StepMethodHandler scenarioMethodHandler, AtomicInt @Override public Object intercept( Object receiver, Method method, Object[] parameters, MethodProxy methodProxy ) throws Throwable { - + long started = System.nanoTime(); InvocationMode mode = getInvocationMode( receiver, method ); - if( methodHandlingEnabled && stackDepth.get() == 0 && !method.getDeclaringClass().equals( Object.class ) ) { + boolean handleMethod = methodHandlingEnabled && stackDepth.get() == 0 && !method.getDeclaringClass().equals( Object.class ); + if( handleMethod ) { scenarioMethodHandler.handleMethod( receiver, method, parameters, mode ); } @@ -45,15 +46,18 @@ public Object intercept( Object receiver, Method method, Object[] parameters, Me stackDepth.incrementAndGet(); return methodProxy.invokeSuper( receiver, parameters ); } catch( Exception t ) { - return handleThrowable( receiver, method, t ); + return handleThrowable( receiver, method, t, System.nanoTime() - started ); } catch( AssertionError e ) { - return handleThrowable( receiver, method, e ); + return handleThrowable( receiver, method, e, System.nanoTime() - started ); } finally { stackDepth.decrementAndGet(); + if( handleMethod ) { + scenarioMethodHandler.handleMethodFinished( System.nanoTime() - started ); + } } } - private Object handleThrowable( Object receiver, Method method, Throwable t ) throws Throwable { + private Object handleThrowable( Object receiver, Method method, Throwable t, long durationInNanos ) throws Throwable { if( methodHandlingEnabled ) { scenarioMethodHandler.handleThrowable( t ); return returnReceiverOrNull( receiver, method ); diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/util/AssertionUtil.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/util/AssertionUtil.java new file mode 100644 index 0000000000..6948e9509d --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/util/AssertionUtil.java @@ -0,0 +1,27 @@ +package com.tngtech.jgiven.impl.util; + +import com.tngtech.jgiven.exception.JGivenInternalDefectException; + +/** + * A collection of methods to assert certain conditions. + * If an asserted condition is false a {@link JGivenInternalDefectException} is thrown. + */ +public class AssertionUtil { + + public static void assertNotNull( Object o ) { + assertNotNull( "Expected a value to not be null, but it apparently was null" ); + } + + public static void assertNotNull( Object o, String msg ) { + if( o == null ) { + throw new JGivenInternalDefectException( msg ); + } + } + + public static void assertTrue( boolean condition, String msg ) { + if( !condition ) { + throw new JGivenInternalDefectException( msg ); + } + } + +} 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 c045f54c1e..cf119922e8 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 @@ -47,12 +47,14 @@ public void visitEnd( ScenarioCaseModel scenarioCase ) { } writer.print( "" ); - if( scenarioCase.success ) { - writer.println( "
Passed
" ); - } else { + writeStatusIcon( scenarioCase.success ); + + if( !scenarioCase.success ) { writer.println( "
Failed: " + scenarioCase.errorMessage + "
" ); } + writeDuration( scenarioCase.durationInNanos ); + writer.print( "" ); writer.println( "" ); diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/HtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/HtmlWriter.java index 7b28a1d5fb..dcec889d6e 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/HtmlWriter.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/HtmlWriter.java @@ -50,6 +50,10 @@ public void writeHtmlFooter() { private void writeJGivenFooter() { writer.print( "" ); } @@ -60,7 +64,7 @@ public void write( ScenarioModel model ) { } public void write( ReportModel model, HtmlTocWriter htmlTocWriter ) { - writeHtmlHeader( model.className ); + writeHtmlHeader( model.getClassName() ); if( htmlTocWriter != null ) { htmlTocWriter.writeToc( writer ); } @@ -70,14 +74,14 @@ public void write( ReportModel model, HtmlTocWriter htmlTocWriter ) { } private void writeStatistics( ReportModel model ) { - if( !model.scenarios.isEmpty() ) { + if( !model.getScenarios().isEmpty() ) { ReportStatistics statistics = new StatisticsCalculator().getStatistics( model ); writer.print( "
" ); writer.print( statistics.numScenarios + " scenarios, " + statistics.numCases + " cases, " + statistics.numSteps + " steps, " + statistics.numFailedCases + " failed cases" ); - writer.println( "
" ); + closeDiv(); } } @@ -139,10 +143,10 @@ void writeHeader( ReportModel reportModel ) { writer.println( "" ); + closeDiv(); } @Override diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ScenarioHtmlWriter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ScenarioHtmlWriter.java index 39c8276798..164a904f7d 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ScenarioHtmlWriter.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/html/ScenarioHtmlWriter.java @@ -1,8 +1,12 @@ package com.tngtech.jgiven.report.html; +import static com.tngtech.jgiven.report.model.ExecutionStatus.FAILED; +import static com.tngtech.jgiven.report.model.ExecutionStatus.SUCCESS; import static java.lang.String.format; import java.io.PrintWriter; +import java.util.Formatter; +import java.util.Locale; import com.google.common.html.HtmlEscapers; import com.tngtech.jgiven.impl.util.WordUtil; @@ -32,7 +36,26 @@ public void visit( ScenarioModel scenarioModel ) { writer.println( "
" ); String id = scenarioModel.className + ":" + scenarioModel.description; - ExecutionStatus executionStatus = scenarioModel.getExecutionStatus(); + + writer.print( format( "

", id ) ); + + writeStatusIcon( scenarioModel.getExecutionStatus() ); + + writer.print( " " + WordUtil.capitalize( scenarioModel.description ) ); + + writeDuration( scenarioModel.getDurationInNanos() ); + writer.println( "

" ); + + writeTagLine( scenarioModel ); + writer.println( "
" ); + writer.println( "
" ); + } + + public void writeStatusIcon( boolean success ) { + writeStatusIcon( success ? SUCCESS : FAILED ); + } + + public void writeStatusIcon( ExecutionStatus executionStatus ) { String iconClass = ""; if( executionStatus == ExecutionStatus.FAILED ) { iconClass = "icon-cancel"; @@ -40,11 +63,7 @@ public void visit( ScenarioModel scenarioModel ) { iconClass = "icon-ok"; } - writer.println( format( "

%s

", - id, iconClass, WordUtil.capitalize( scenarioModel.description ) ) ); - writeTagLine( scenarioModel ); - writer.println( "
" ); - writer.println( "
" ); + writer.print( format( "", iconClass ) ); } private void writeTagLine( ScenarioModel scenarioModel ) { @@ -89,7 +108,9 @@ private String getCaseId() { void printCaseHeader( ScenarioCaseModel scenarioCase ) { writer.println( format( "
", scenarioCase.success ? "passed" : "failed" ) ); if( !scenarioCase.arguments.isEmpty() ) { - writer.print( format( "

Case %d: ", getCaseId(), scenarioCase.caseNr ) ); + writer.print( format( "

", getCaseId() ) ); + writeStatusIcon( scenarioCase.success ); + writer.print( format( " Case %d: ", scenarioCase.caseNr ) ); for( int i = 0; i < scenarioCase.arguments.size(); i++ ) { if( scenarioModel.parameterNames.size() > i ) { @@ -102,15 +123,15 @@ void printCaseHeader( ScenarioCaseModel scenarioCase ) { writer.print( ", " ); } } + + writeDuration( scenarioCase.durationInNanos ); writer.println( "

" ); } } @Override public void visitEnd( ScenarioCaseModel scenarioCase ) { - if( scenarioCase.success ) { - writer.println( "
Passed
" ); - } else { + if( !scenarioCase.success ) { writer.println( "
Failed: " + scenarioCase.errorMessage + "
" ); } writer.println( "" ); @@ -137,14 +158,26 @@ public void visit( StepModel stepModel ) { } firstWord = false; } + StepStatus status = stepModel.getStatus(); if( status != StepStatus.PASSED ) { String lowerCase = status.toString().toLowerCase(); writer.print( format( " %s", WordUtil.camelCase( lowerCase ), lowerCase.replace( '_', ' ' ) ) ); } + + writeDuration( stepModel.getDurationInNanos() ); + writer.println( "" ); } + protected void writeDuration( long durationInNanos ) { + // TODO: provide a configuration value to configure the locale + double durationInMs = ( (double) durationInNanos ) / 1000000; + Formatter usFormatter = new Formatter( Locale.US ); + writer.print( usFormatter.format( " (%.2f ms)", durationInMs ) ); + usFormatter.close(); + } + private void printArg( Word word ) { String value = word.getArgumentInfo().isCaseArg() ? formatCaseArgument( word ) : HtmlEscapers.htmlEscaper().escape( word.value ); value = escapeToHtml( value ); 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 82780e9a60..162267f537 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 @@ -75,7 +75,7 @@ public void handleReportModel( ReportModel model, File file ) { modelFile.file = targetFile; models.add( modelFile ); - for( ScenarioModel scenario : model.scenarios ) { + for( ScenarioModel scenario : model.getScenarios() ) { for( Tag tag : scenario.tags ) { addToMap( tag, scenario ); } @@ -107,7 +107,7 @@ private void writeIndexFile( HtmlTocWriter tocWriter ) { htmlWriter.writeHtmlHeader( "Scenarios" ); ReportModel reportModel = new ReportModel(); - reportModel.className = ".Scenarios"; + reportModel.setClassName( ".Scenarios" ); tocWriter.writeToc( printWriter ); htmlWriter.visit( reportModel ); @@ -135,12 +135,12 @@ private void writeTagFiles( HtmlTocWriter tocWriter ) { private void writeTagFile( Tag tag, List value, HtmlTocWriter tocWriter ) { try { ReportModel reportModel = new ReportModel(); - reportModel.className = tag.getName(); + reportModel.setClassName( tag.getName() ); if( tag.getValue() != null ) { - reportModel.className += "." + tag.getValueString(); + reportModel.setClassName( reportModel.getClassName() + "." + tag.getValueString() ); } - reportModel.scenarios = value; - reportModel.description = tag.getDescription(); + reportModel.setScenarios( value ); + reportModel.setDescription( tag.getDescription() ); String fileName = HtmlTocWriter.tagToFilename( tag ); File targetFile = new File( toDir, fileName ); diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/impl/CaseArgumentAnalyser.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/impl/CaseArgumentAnalyser.java index 0e08086bb8..d5789328b4 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/impl/CaseArgumentAnalyser.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/impl/CaseArgumentAnalyser.java @@ -25,7 +25,7 @@ public class CaseArgumentAnalyser { private static final Logger log = LoggerFactory.getLogger( CaseArgumentAnalyser.class ); public void analyze( ReportModel model ) { - for( ScenarioModel scenarioModel : model.scenarios ) { + for( ScenarioModel scenarioModel : model.getScenarios() ) { analyze( scenarioModel ); } } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/impl/CommonReportHelper.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/impl/CommonReportHelper.java index e25fae720e..357144a770 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/impl/CommonReportHelper.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/impl/CommonReportHelper.java @@ -33,7 +33,7 @@ public void finishReport( ReportModel model ) { if( !reportDir.exists() && !reportDir.mkdirs() ) { log.error( "Could not create report directory " + reportDir ); } - File reportFile = new File( reportDir, model.className + ".json" ); + File reportFile = new File( reportDir, model.getClassName() + ".json" ); log.info( "Writing scenario report to file " + reportFile.getAbsolutePath() ); new ScenarioJsonWriter( model ).write( reportFile ); } 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 ed18720698..f0c3acad87 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 @@ -12,14 +12,14 @@ public class ReportModel { /** * Full qualified name of the test class. */ - public String className; + private String className; /** * An optional description of the test class. */ - public String description; + private String description; - public List scenarios = Lists.newArrayList(); + private List scenarios = Lists.newArrayList(); public void accept( ReportModelVisitor visitor ) { visitor.visit( this ); @@ -32,7 +32,7 @@ public void accept( ReportModelVisitor visitor ) { } private List sortByDescription() { - List sorted = Lists.newArrayList( scenarios ); + List sorted = Lists.newArrayList( getScenarios() ); Collections.sort( sorted, new Comparator() { @Override public int compare( ScenarioModel o1, ScenarioModel o2 ) { @@ -43,7 +43,7 @@ public int compare( ScenarioModel o1, ScenarioModel o2 ) { } public ScenarioModel getLastScenarioModel() { - return scenarios.get( scenarios.size() - 1 ); + return getScenarios().get( getScenarios().size() - 1 ); } public StepModel getFirstStepModelOfLastScenario() { @@ -51,11 +51,35 @@ public StepModel getFirstStepModelOfLastScenario() { } public void addScenarioModel( ScenarioModel currentScenarioModel ) { - scenarios.add( currentScenarioModel ); + getScenarios().add( currentScenarioModel ); } public String getSimpleClassName() { - return Iterables.getLast( Splitter.on( '.' ).split( className ) ); + return Iterables.getLast( Splitter.on( '.' ).split( getClassName() ) ); + } + + public String getDescription() { + return description; + } + + public void setDescription( String description ) { + this.description = description; + } + + public String getClassName() { + return className; + } + + public void setClassName( String className ) { + this.className = className; + } + + public List getScenarios() { + return scenarios; + } + + public void setScenarios( List scenarios ) { + this.scenarios = scenarios; } } 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 6a12d75677..d6f3c4b03f 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 @@ -31,6 +31,7 @@ import com.tngtech.jgiven.impl.NamedArgument; import com.tngtech.jgiven.impl.intercept.InvocationMode; import com.tngtech.jgiven.impl.intercept.ScenarioListener; +import com.tngtech.jgiven.impl.util.AssertionUtil; import com.tngtech.jgiven.impl.util.WordUtil; import com.tngtech.jgiven.report.model.StepFormatter.Formatting; @@ -48,6 +49,8 @@ public class ReportModelBuilder implements ScenarioListener { private AbstractJGivenConfiguraton configuration = new DefaultConfiguration(); + private long scenarioStartedNanos; + public ReportModelBuilder() { this( new ReportModel() ); } @@ -62,6 +65,7 @@ public void setReportModel( ReportModel reportModel ) { @Override public void scenarioStarted( String description ) { + scenarioStartedNanos = System.nanoTime(); String readableDescription = description; if( description.contains( "_" ) ) { @@ -72,8 +76,8 @@ public void scenarioStarted( String description ) { currentScenarioCase = new ScenarioCaseModel(); - if( !reportModel.scenarios.isEmpty() ) { - ScenarioModel scenarioModel = reportModel.scenarios.get( reportModel.scenarios.size() - 1 ); + if( !reportModel.getScenarios().isEmpty() ) { + ScenarioModel scenarioModel = reportModel.getScenarios().get( reportModel.getScenarios().size() - 1 ); if( scenarioModel.description.equals( readableDescription ) ) { currentScenarioModel = scenarioModel; } @@ -81,8 +85,8 @@ public void scenarioStarted( String description ) { if( currentScenarioModel == null ) { currentScenarioModel = new ScenarioModel(); - currentScenarioModel.className = reportModel.className; - reportModel.scenarios.add( currentScenarioModel ); + currentScenarioModel.className = reportModel.getClassName(); + reportModel.getScenarios().add( currentScenarioModel ); } currentScenarioModel.addCase( currentScenarioCase ); @@ -187,7 +191,7 @@ public void setParameterNames( List parameterNames ) { } public void setClassName( String name ) { - reportModel.className = name; + reportModel.setClassName( name ); } public void setSuccess( boolean success ) { @@ -214,6 +218,13 @@ public void stepMethodFailed( Throwable t ) { } } + @Override + public void stepMethodFinished( long durationInNanos ) { + if( !currentScenarioCase.steps.isEmpty() ) { + currentScenarioCase.steps.get( currentScenarioCase.steps.size() - 1 ).setDurationInNanos( durationInNanos ); + } + } + @Override public void scenarioFailed( Throwable e ) { setSuccess( false ); @@ -366,4 +377,12 @@ private static List getExplodedTags( Tag originalTag, String[] stringArray return result; } + @Override + public void scenarioFinished() { + AssertionUtil.assertTrue( scenarioStartedNanos > 0, "Scenario has no start time" ); + long durationInNanos = System.nanoTime() - scenarioStartedNanos; + currentScenarioCase.setDurationInNanoes( durationInNanos ); + currentScenarioModel.addDurationInNanos( durationInNanos ); + } + } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioCaseModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioCaseModel.java index 34e275c8e2..f8907acfd5 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioCaseModel.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/ScenarioCaseModel.java @@ -13,6 +13,11 @@ public class ScenarioCaseModel { public boolean success = true; public String errorMessage; + /** + * The total execution time of the whole case in milliseconds + */ + public long durationInNanos; + public StepModel addStep( String name, List words, InvocationMode mode ) { StepModel stepModel = new StepModel(); stepModel.name = name; @@ -56,4 +61,12 @@ public void addStep( StepModel stepModel ) { public StepModel getStep( int i ) { return steps.get( i ); } + + public void setDurationInNanoes( long durationInNanos ) { + this.durationInNanos = durationInNanos; + } + + public long getDurationInNanos() { + return durationInNanos; + } } \ No newline at end of file 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 9fbab0449e..851aef52bc 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 @@ -16,6 +16,7 @@ public class ScenarioModel { public List parameterNames = Lists.newArrayList(); private boolean casesAsTable; private final List scenarioCases = Lists.newArrayList(); + private long durationInNanos; public void accept( ReportModelVisitor visitor ) { visitor.visit( this ); @@ -100,4 +101,16 @@ public void setCasesAsTable( boolean casesAsTable ) { public void clearCases() { scenarioCases.clear(); } + + public long getDurationInNanos() { + return durationInNanos; + } + + public void setDurationInNanos( long durationInNanos ) { + this.durationInNanos = durationInNanos; + } + + public void addDurationInNanos( long durationInNanosDelta ) { + this.durationInNanos += durationInNanosDelta; + } } \ No newline at end of file diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/StepModel.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/StepModel.java index 74fad1f6bf..0075b9e845 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/StepModel.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/model/StepModel.java @@ -11,6 +11,11 @@ public class StepModel { public List words = Lists.newArrayList(); private StepStatus status = StepStatus.PASSED; + /** + * The total execution time of the step in nano seconds + */ + private long durationInNanos; + public void accept( ReportModelVisitor visitor ) { visitor.visit( this ); } @@ -43,4 +48,12 @@ public StepStatus getStatus() { public void setStatus( StepStatus status ) { this.status = status; } + + public long getDurationInNanos() { + return durationInNanos; + } + + public void setDurationInNanos( long durationInNanos ) { + this.durationInNanos = durationInNanos; + } } \ No newline at end of file diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/report/text/PlainTextReporter.java b/jgiven-core/src/main/java/com/tngtech/jgiven/report/text/PlainTextReporter.java index 999f75f72e..b2e89aaf9a 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/report/text/PlainTextReporter.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/report/text/PlainTextReporter.java @@ -64,7 +64,7 @@ public void write( ScenarioModel scenarioModel ) { public void visit( ReportModel multiScenarioModel ) { writer.println(); String title = withColor( Color.RED, INTENSITY_BOLD, "Test Class: " ); - title += withColor( Color.RED, multiScenarioModel.className ); + title += withColor( Color.RED, multiScenarioModel.getClassName() ); println( Color.RED, title ); } diff --git a/jgiven-core/src/main/resources/com/tngtech/jgiven/report/html/default.css b/jgiven-core/src/main/resources/com/tngtech/jgiven/report/html/default.css index c562480714..13c39fd3e5 100644 --- a/jgiven-core/src/main/resources/com/tngtech/jgiven/report/html/default.css +++ b/jgiven-core/src/main/resources/com/tngtech/jgiven/report/html/default.css @@ -280,11 +280,11 @@ h1 { cursor: pointer; } -.scenario h3 .icon-ok { +.icon-ok { color: green; } -.scenario h3 .icon-cancel { +.icon-cancel { color: red; } @@ -556,5 +556,10 @@ ul.steps li { } .statistics { - padding-top:10px; + padding-top:10px; +} + +.duration { + opacity: 0.5; + font-size: 10px; } diff --git a/jgiven-core/src/test/java/com/tngtech/jgiven/ThenTestStep.java b/jgiven-core/src/test/java/com/tngtech/jgiven/ThenTestStep.java index 11a5419bef..c3e6a123c5 100644 --- a/jgiven-core/src/test/java/com/tngtech/jgiven/ThenTestStep.java +++ b/jgiven-core/src/test/java/com/tngtech/jgiven/ThenTestStep.java @@ -11,8 +11,9 @@ public class ThenTestStep extends Stage { public void sms_and_emails_exist() {} - public void the_result_is( int i ) { + public ThenTestStep the_result_is( int i ) { assertThat( intResult ).isEqualTo( i ); + return self(); } public ThenTestStep something_has_happend() { diff --git a/jgiven-core/src/test/java/com/tngtech/jgiven/report/html/HtmlWriterTest.java b/jgiven-core/src/test/java/com/tngtech/jgiven/report/html/HtmlWriterTest.java index 8943505428..cb39a81f02 100644 --- a/jgiven-core/src/test/java/com/tngtech/jgiven/report/html/HtmlWriterTest.java +++ b/jgiven-core/src/test/java/com/tngtech/jgiven/report/html/HtmlWriterTest.java @@ -47,7 +47,6 @@ public void HTML_report_is_correctly_generated_for_scenarios( int a, int b, int + "
  • Given " + a + ".*and.*" + b + ".*
  • .*" + "
  • When both values are multiplied with each other.*
  • .*" + "
  • Then the result is.*" + expectedResult + ".*
  • .*" - + "
    Passed
    " + ".*" ); } @@ -71,7 +70,7 @@ public void tests_with_arguments_generate_cases() throws Exception { getScenario().finished(); ReportModel model = getScenario().getModel(); String string = HtmlWriter.toString( model.getLastScenarioModel() ); - assertThat( string.replace( '\n', ' ' ) ).matches( ".*Case 1: paramA = 1, paramB = b.*" ); + assertThat( string.replace( '\n', ' ' ) ).matches( ".*.*Case 1: paramA = 1, paramB = b.*.*" ); } } diff --git a/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/DataProviderTest.java b/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/DataProviderTest.java index 54da609e30..d07a8cd5ca 100644 --- a/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/DataProviderTest.java +++ b/jgiven-junit/src/test/java/com/tngtech/jgiven/junit/DataProviderTest.java @@ -36,7 +36,7 @@ public void DataProviderRunner_can_be_used( int intArg, boolean booleanArg, int when().multiply_with_two(); then().the_value_is_$not$_greater_than_zero( booleanArg ); - ScenarioModel scenarioModel = getScenario().getModel().scenarios.get( 0 ); + ScenarioModel scenarioModel = getScenario().getModel().getScenarios().get( 0 ); List arguments = scenarioModel.getCase( caseNr ).arguments; assertThat( arguments ).containsExactly( "" + intArg, "" + booleanArg, "" + caseNr ); } @@ -59,7 +59,7 @@ public void DataProviderRunner_with_tricky_data( int firstArg, int secondArg, in when().multiply_with_two(); - ScenarioModel scenarioModel = getScenario().getModel().scenarios.get( 0 ); + ScenarioModel scenarioModel = getScenario().getModel().getScenarios().get( 0 ); if( scenarioModel.getScenarioCases().size() == 3 ) { CaseArgumentAnalyser analyser = new CaseArgumentAnalyser(); analyser.analyze( scenarioModel ); diff --git a/jgiven-testng/src/main/java/com/tngtech/jgiven/testng/ScenarioTestListener.java b/jgiven-testng/src/main/java/com/tngtech/jgiven/testng/ScenarioTestListener.java index 9fde13b19a..205370ee52 100644 --- a/jgiven-testng/src/main/java/com/tngtech/jgiven/testng/ScenarioTestListener.java +++ b/jgiven-testng/src/main/java/com/tngtech/jgiven/testng/ScenarioTestListener.java @@ -27,7 +27,7 @@ public class ScenarioTestListener implements ITestListener { @Override public void onTestStart( ITestResult paramITestResult ) { Object instance = paramITestResult.getInstance(); - scenarioCollectionModel.className = instance.getClass().getName(); + scenarioCollectionModel.setClassName( instance.getClass().getName() ); if( instance instanceof ScenarioTestBase ) { ScenarioTestBase testInstance = (ScenarioTestBase) instance; diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/HtmlWriterScenarioTest.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/HtmlWriterScenarioTest.java index 0b804eecf2..88f32c62ee 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/HtmlWriterScenarioTest.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/HtmlWriterScenarioTest.java @@ -12,6 +12,7 @@ import com.tngtech.jgiven.report.model.GivenReportModel; import com.tngtech.jgiven.report.model.StepStatus; import com.tngtech.jgiven.tags.FeatureDataTables; +import com.tngtech.jgiven.tags.FeatureDuration; import com.tngtech.jgiven.tags.FeatureHtmlReport; import com.tngtech.jgiven.tags.Issue; @@ -23,10 +24,10 @@ public class HtmlWriterScenarioTest extends ScenarioTest, Wh @DataProvider public static Object[][] statusTexts() { return new Object[][] { - { StepStatus.PASSED, "something happens" }, - { StepStatus.FAILED, "something happens failed" }, - { StepStatus.SKIPPED, "something happens skipped" }, - { StepStatus.NOT_IMPLEMENTED_YET, "something happens not implemented yet" }, + { StepStatus.PASSED, "something happens.*" }, + { StepStatus.FAILED, "something happens failed.*" }, + { StepStatus.SKIPPED, "something happens skipped.*" }, + { StepStatus.NOT_IMPLEMENTED_YET, "something happens not implemented yet.*" }, }; } @@ -37,7 +38,7 @@ public void step_status_appears_correctly_in_HTML_reports( StepStatus status, St .and().step_$_is_named( 1, "something happens" ) .and().step_$_has_status( 1, status ); when().the_HTML_report_is_generated(); - then().the_HTML_report_contains_text( expectedString ); + then().the_HTML_report_contains_pattern( expectedString ); } @Test @@ -46,7 +47,7 @@ public void HTML_in_arguments_is_escaped_in_HTML_reports() { given().a_report_model_with_one_scenario() .and().case_$_has_a_when_step_$_with_argument( 1, "test", "" ); when().the_HTML_report_is_generated(); - then().the_HTML_report_contains_text( "<someHtmlTag>" ); + then().the_HTML_report_contains_pattern( "<someHtmlTag>" ); } @Test @@ -74,8 +75,8 @@ public void when_data_tables_are_generated_then_step_parameter_placeholders_are_ .and().case_$_has_a_when_step_$_with_argument( 2, "uses the second parameter", "b" ); when().the_HTML_report_is_generated(); - then().the_HTML_report_contains_text( "uses the first parameter.*<param1>.*second" ) - .and().the_HTML_report_contains_text( "uses the second parameter.*<param2>.*Cases" ) + then().the_HTML_report_contains_pattern( "uses the first parameter.*<param1>.*second" ) + .and().the_HTML_report_contains_pattern( "uses the second parameter.*<param2>.*Cases" ) .and().the_HTML_report_contains_a_data_table_with_header_values( "param1", "param2" ) .and().the_data_table_has_one_line_for_the_arguments_of_each_case(); } @@ -88,4 +89,23 @@ public void the_error_message_of_failed_scenarios_are_reported() { when().the_HTML_report_is_generated(); then().the_HTML_report_contains_text( "
    Failed: Test Error
    " ); } + + @Test + @FeatureDuration + public void the_duration_of_steps_are_reported() { + given().a_report_model_with_one_scenario() + .and().step_$_has_a_duration_of_$_nano_seconds( 1, 123456789 ); + when().the_HTML_report_is_generated(); + then().the_HTML_report_contains_text( "(123.46 ms)" ); + } + + @Test + @FeatureDuration + public void the_duration_of_scenarios_are_reported() { + given().a_report_model_with_one_scenario() + .and().the_scenario_has_a_duration_of_$_nano_seconds( 123456789 ); + when().the_HTML_report_is_generated(); + then().the_HTML_report_contains_text( "(123.46 ms)" ); + } + } diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/ThenHtmlOutput.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/ThenHtmlOutput.java index 6b9368925f..619d0d12dc 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/ThenHtmlOutput.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/ThenHtmlOutput.java @@ -25,7 +25,7 @@ public ThenHtmlOutput the_HTML_report_contains_a_data_table_with_header_values( } patternBuilder.append( "\\s*Status\\s*" ); patternBuilder.append( "\\s*.*.*" ); - return the_HTML_report_contains_text( patternBuilder.toString() ); + return the_HTML_report_contains_pattern( patternBuilder.toString() ); } public ThenHtmlOutput the_data_table_has_one_line_for_the_arguments_of_each_case() { @@ -37,17 +37,21 @@ public ThenHtmlOutput the_data_table_has_one_line_for_the_arguments_of_each_case for( String arg : caseModel.arguments ) { patternBuilder.append( "\\s*" + arg + "\\s*" ); } - patternBuilder.append( "\\s*.*Passed.*\\s*" ); + patternBuilder.append( "\\s*.*icon-ok.*\\s*" ); patternBuilder.append( "\\s*" ); } patternBuilder.append( "\\s*.*" ); - return the_HTML_report_contains_text( patternBuilder.toString() ); + return the_HTML_report_contains_pattern( patternBuilder.toString() ); } - public ThenHtmlOutput the_HTML_report_contains_text( String string ) { - Pattern pattern = Pattern.compile( ".*" + string + ".*", Pattern.MULTILINE | Pattern.DOTALL ); + public ThenHtmlOutput the_HTML_report_contains_pattern( String patternString ) { + Pattern pattern = Pattern.compile( ".*" + patternString + ".*", Pattern.MULTILINE | Pattern.DOTALL ); Assertions.assertThat( html ).matches( pattern ); return self(); } + public ThenHtmlOutput the_HTML_report_contains_text( String text ) { + Assertions.assertThat( html ).contains( text ); + return self(); + } } diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/ThenStaticHtmlReportGenerator.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/ThenStaticHtmlReportGenerator.java index bd4772df26..59b9022fdb 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/ThenStaticHtmlReportGenerator.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/html/ThenStaticHtmlReportGenerator.java @@ -19,7 +19,7 @@ public SELF an_index_file_exists() { public SELF an_HTML_file_exists_for_each_test_class() { for( ReportModel model : reportModels ) { - a_file_with_name_$_exists( model.className + ".html" ); + a_file_with_name_$_exists( model.getClassName() + ".html" ); } return self(); } diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/json/GivenJsonReports.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/json/GivenJsonReports.java index 9e927dde9d..dadaefc9f1 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/json/GivenJsonReports.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/json/GivenJsonReports.java @@ -34,7 +34,7 @@ public SELF the_reports_exist_as_JSON_files() throws IOException { jsonReportDirectory = temporaryFolderRule.newFolder(); for( ReportModel reportModel : reportModels ) { - File jsonReportFile = new File( jsonReportDirectory, reportModel.className + ".json" ); + File jsonReportFile = new File( jsonReportDirectory, reportModel.getClassName() + ".json" ); jsonReportFiles.add( jsonReportFile ); new ScenarioJsonWriter( reportModel ).write( jsonReportFile ); diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModel.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModel.java index 824aa4e0c5..a5f6d43b60 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModel.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/model/GivenReportModel.java @@ -22,7 +22,7 @@ public SELF a_report_model_with_one_scenario() { public SELF a_report_model() { reportModel = new ReportModel(); - reportModel.className = "Test Class"; + reportModel.setClassName( "Test Class" ); createScenarioModel( "something should happen", "something_should_happen" ); @@ -31,13 +31,13 @@ public SELF a_report_model() { private void createScenarioModel( String description, String testMethodName ) { ScenarioModel scenarioModel = new ScenarioModel(); - scenarioModel.className = reportModel.className; + scenarioModel.className = reportModel.getClassName(); scenarioModel.description = description; scenarioModel.testMethodName = testMethodName; addCase( scenarioModel ); - reportModel.scenarios.add( scenarioModel ); + reportModel.getScenarios().add( scenarioModel ); } private void addCase( ScenarioModel scenarioModel ) { @@ -57,15 +57,15 @@ private void addCase( ScenarioModel scenarioModel ) { public SELF a_report_model_with_name( String name ) { a_report_model(); - reportModel.className = name; - for( ScenarioModel model : reportModel.scenarios ) { + reportModel.setClassName( name ); + for( ScenarioModel model : reportModel.getScenarios() ) { model.className = name; } return self(); } public void the_report_has_$_scenarios( int n ) { - reportModel.scenarios.clear(); + reportModel.getScenarios().clear(); for( int i = 0; i < n; i++ ) { createScenarioModel( "something should happen " + i, "something_should_happen_" + i ); } @@ -80,6 +80,11 @@ public SELF the_scenario_has_parameters( String... params ) { return self(); } + public SELF the_scenario_has_a_duration_of_$_nano_seconds( long durationInNanos ) { + reportModel.getLastScenarioModel().setDurationInNanos( durationInNanos ); + return self(); + } + public SELF the_scenario_has_$_cases( int ncases ) { reportModel.getLastScenarioModel().clearCases(); for( int i = 0; i < ncases; i++ ) { @@ -127,6 +132,11 @@ private ScenarioCaseModel getCase( int ncase ) { return self(); } + public SELF step_$_has_a_duration_of_$_nano_seconds( int i, long durationInNanos ) { + getCase( 1 ).getStep( i - 1 ).setDurationInNanos( durationInNanos ); + return self(); + } + public SELF case_$_has_a_when_step_$_with_argument( int ncase, String name, String arg ) { getCase( ncase ).addStep( name, Arrays.asList( Word.introWord( "when" ), new Word( name ), Word.argWord( arg ) ), InvocationMode.NORMAL ); @@ -138,7 +148,7 @@ public SELF the_first_scenario_has_tag( String name ) { } public SELF scenario_$_has_tag_$_with_value_$( int i, String name, String value ) { - reportModel.scenarios.get( i - 1 ).tags.add( new Tag( name, value ).setPrependType( true ) ); + reportModel.getScenarios().get( i - 1 ).tags.add( new Tag( name, value ).setPrependType( true ) ); return self(); } diff --git a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/text/ThenPlainTextReportGenerator.java b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/text/ThenPlainTextReportGenerator.java index e95b341692..cae1f914e6 100644 --- a/jgiven-tests/src/test/java/com/tngtech/jgiven/report/text/ThenPlainTextReportGenerator.java +++ b/jgiven-tests/src/test/java/com/tngtech/jgiven/report/text/ThenPlainTextReportGenerator.java @@ -7,7 +7,7 @@ public class ThenPlainTextReportGenerator