diff --git a/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java b/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java index 305620f710..ce3b6109ab 100644 --- a/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java +++ b/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java @@ -399,16 +399,20 @@ private void handleWhereBlock(Method method) { public void visitMethodAgain(Method method) { this.block = null; - if (!movedStatsBackToMethod) + if (!movedStatsBackToMethod) { for (Block b : method.getBlocks()) { - // this will only have the blocks if there was no 'cleanup' block in the method + // This will only run if there was no 'cleanup' block in the method. + // Otherwise, the blocks have already been copied to try block by visitCleanupBlock. + // We need to run as late as possible, so we'll have to do the handling here and in visitCleanupBlock. addBlockListeners(b); method.getStatements().addAll(b.getAst()); } + } // for global required interactions - if (method instanceof FeatureMethod) + if (method instanceof FeatureMethod) { method.getStatements().add(createMockControllerCall(nodeCache.MockController_LeaveScope)); + } if (methodHasCondition) { defineValueRecorder(method.getStatements(), ""); @@ -423,6 +427,7 @@ private void addBlockListeners(Block block) { BlockParseInfo blockType = block.getParseInfo(); if (blockType == BlockParseInfo.WHERE || blockType == BlockParseInfo.METHOD_END + || blockType == BlockParseInfo.COMBINED || blockType == BlockParseInfo.ANONYMOUS) return; // SpockRuntime.enterBlock(getSpecificationContext(), new BlockInfo(blockKind, [blockTexts])) @@ -430,9 +435,10 @@ private void addBlockListeners(Block block) { // SpockRuntime.exitedBlock(getSpecificationContext(), new BlockInfo(blockKind, [blockTexts])) MethodCallExpression exitBlockCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallExitBlock); - // As the cleanup block finalizes the specification, it would override any previous block in ErrorInfo, - // so we only call enterBlock if there is no error yet. if (blockType == BlockParseInfo.CLEANUP) { + // In case of a cleanup block we need store a reference of the previously `currentBlock` in case that an exception occurred + // and restore it at the end of the cleanup block, so that the correct `BlockInfo` is available for the `IErrorContext`. + // The restoration happens in the `finally` statement created by `createCleanupTryCatch`. VariableExpression failedBlock = new VariableExpression(SpockNames.FAILED_BLOCK, nodeCache.BlockInfo); block.getAst().addAll(0, asList( ifThrowableIsNotNull(storeFailedBlock(failedBlock)), @@ -451,7 +457,6 @@ private void addBlockListeners(Block block) { } private @NotNull Statement restoreFailedBlock(VariableExpression failedBlock) { - return new ExpressionStatement(createDirectMethodCall(new CastExpression(nodeCache.SpecificationContext, getSpecificationContext()), nodeCache.SpecificationContext_SetBlockCurrentBlock, new ArgumentListExpression(failedBlock))); } diff --git a/spock-core/src/main/java/org/spockframework/runtime/ErrorContext.java b/spock-core/src/main/java/org/spockframework/runtime/ErrorContext.java index 34f670df9e..8beeff184e 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/ErrorContext.java +++ b/spock-core/src/main/java/org/spockframework/runtime/ErrorContext.java @@ -1,6 +1,7 @@ package org.spockframework.runtime; import org.spockframework.runtime.model.*; +import org.spockframework.util.Nullable; class ErrorContext implements IErrorContext { private final SpecInfo spec; @@ -8,7 +9,7 @@ class ErrorContext implements IErrorContext { private final IterationInfo iteration; private final BlockInfo block; - private ErrorContext(SpecInfo spec, FeatureInfo feature, IterationInfo iteration, BlockInfo block) { + private ErrorContext(@Nullable SpecInfo spec, @Nullable FeatureInfo feature, @Nullable IterationInfo iteration, @Nullable BlockInfo block) { this.spec = spec; this.feature = feature; this.iteration = iteration; @@ -43,4 +44,13 @@ public IterationInfo getCurrentIteration() { public BlockInfo getCurrentBlock() { return block; } + + @Override + public String toString() { + return "ErrorContext{Spec: " + (spec == null ? "null" : spec.getDisplayName()) + + ", Feature: " + (feature == null ? "null" : feature.getDisplayName()) + + ", Iteration: " + (iteration == null ? "null" : iteration.getDisplayName()) + + ", Block: " + (block == null ? "null" : (block.getKind() + " " + block.getTexts())) + + "}"; + } } diff --git a/spock-core/src/main/java/org/spockframework/runtime/IRunListener.java b/spock-core/src/main/java/org/spockframework/runtime/IRunListener.java index 1764c54565..6c27252faa 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/IRunListener.java +++ b/spock-core/src/main/java/org/spockframework/runtime/IRunListener.java @@ -21,7 +21,9 @@ * Listens to a spec run. Currently, only extensions can register listeners. * They do so by invoking SpecInfo.addListener(). See * {@link StepwiseExtension} for an example of how to use a listener. + *

* + * @see org.spockframework.runtime.extension.IBlockListener * @author Peter Niederwieser */ public interface IRunListener { diff --git a/spock-core/src/main/java/org/spockframework/runtime/SpecInfoBuilder.java b/spock-core/src/main/java/org/spockframework/runtime/SpecInfoBuilder.java index 7c7f4445fb..4eb810a245 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/SpecInfoBuilder.java +++ b/spock-core/src/main/java/org/spockframework/runtime/SpecInfoBuilder.java @@ -169,10 +169,7 @@ private FeatureInfo createFeature(Method method, FeatureMetadata featureMetadata } for (BlockMetadata blockMetadata : featureMetadata.blocks()) { - BlockInfo block = new BlockInfo(); - block.setKind(blockMetadata.kind()); - block.setTexts(asList(blockMetadata.texts())); - feature.addBlock(block); + feature.addBlock(new BlockInfo(blockMetadata.kind(), asList(blockMetadata.texts()))); } return feature; diff --git a/spock-core/src/main/java/org/spockframework/runtime/extension/IBlockListener.java b/spock-core/src/main/java/org/spockframework/runtime/extension/IBlockListener.java index 3cedac71e5..32bd914922 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/extension/IBlockListener.java +++ b/spock-core/src/main/java/org/spockframework/runtime/extension/IBlockListener.java @@ -1,9 +1,43 @@ package org.spockframework.runtime.extension; import org.spockframework.runtime.model.BlockInfo; +import org.spockframework.runtime.model.ErrorInfo; import org.spockframework.runtime.model.IterationInfo; +import org.spockframework.util.Beta; +/** + * Listens to block events during the execution of a feature. + *

+ * Usually used in conjunction with {@link org.spockframework.runtime.IRunListener}. + * Currently, only extensions can register listeners. + * They do so by invoking {@link org.spockframework.runtime.model.FeatureInfo#addBlockListener(IBlockListener)}. + * It is preferred to use a single instance of this. + *

+ * It is discouraged to perform long-running operations in the listener methods, + * as they are called during the execution of the specification. + * It is discouraged to perform any side effects affecting the tests. + *

+ * When an exception is thrown in a block, the {@code blockExited} will not be called for that block. + * If a cleanup block is present the cleanup block listener methods will still be called. + * + * @see org.spockframework.runtime.IRunListener + * @author Leonard Brünings + * @since 2.4 + */ +@Beta public interface IBlockListener { + + /** + * Called when a block is entered. + */ default void blockEntered(IterationInfo iterationInfo, BlockInfo blockInfo) {} + + /** + * Called when a block is exited. + *

+ * This method is not called if an exception is thrown in the block. + * The block that was active will be available in the {@link org.spockframework.runtime.model.IErrorContext} + * and can be observed via {@link org.spockframework.runtime.IRunListener#error(ErrorInfo)}. + */ default void blockExited(IterationInfo iterationInfo, BlockInfo blockInfo) {} } diff --git a/spock-core/src/main/java/org/spockframework/runtime/model/BlockInfo.java b/spock-core/src/main/java/org/spockframework/runtime/model/BlockInfo.java index 0a19bf6564..3cac692cbf 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/model/BlockInfo.java +++ b/spock-core/src/main/java/org/spockframework/runtime/model/BlockInfo.java @@ -16,6 +16,8 @@ package org.spockframework.runtime.model; +import org.spockframework.util.Nullable; + import java.util.List; /** @@ -35,6 +37,7 @@ public BlockInfo(BlockKind kind, List texts) { this.texts = texts; } + @Nullable public BlockKind getKind() { return kind; } @@ -43,6 +46,7 @@ public void setKind(BlockKind kind) { this.kind = kind; } + @Nullable public List getTexts() { return texts; } diff --git a/spock-core/src/main/java/org/spockframework/runtime/model/ErrorInfo.java b/spock-core/src/main/java/org/spockframework/runtime/model/ErrorInfo.java index a978c325e2..7a5c0f29b9 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/model/ErrorInfo.java +++ b/spock-core/src/main/java/org/spockframework/runtime/model/ErrorInfo.java @@ -36,4 +36,13 @@ public Throwable getException() { public IErrorContext getErrorContext() { return errorContext; } + + @Override + public String toString() { + return "ErrorInfo{" + + "method=" + method + + ", errorContext=" + errorContext + + ", error=" + error + + '}'; + } } diff --git a/spock-core/src/main/java/org/spockframework/runtime/model/FeatureInfo.java b/spock-core/src/main/java/org/spockframework/runtime/model/FeatureInfo.java index 00c5e456a9..53ee0d5744 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/model/FeatureInfo.java +++ b/spock-core/src/main/java/org/spockframework/runtime/model/FeatureInfo.java @@ -168,7 +168,7 @@ public List getInitializerInterceptors() { } /** - * Adds a initializer interceptor for this feature. + * Adds an initializer interceptor for this feature. *

* The feature-scoped interceptors will execute before the spec interceptors. * @@ -224,10 +224,18 @@ public void addIterationInterceptor(IMethodInterceptor interceptor) { iterationInterceptors.add(interceptor); } + /** + * @since 2.4 + */ + @Beta public List getBlockListeners() { return blockListeners; } + /** + * @since 2.4 + */ + @Beta public void addBlockListener(IBlockListener blockListener) { blockListeners.add(blockListener); } diff --git a/spock-core/src/main/java/org/spockframework/runtime/model/IErrorContext.java b/spock-core/src/main/java/org/spockframework/runtime/model/IErrorContext.java index 42c7c7317c..3ddbf1a0df 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/model/IErrorContext.java +++ b/spock-core/src/main/java/org/spockframework/runtime/model/IErrorContext.java @@ -2,6 +2,11 @@ import org.spockframework.util.Nullable; +/** + * Provides context information for an error that occurred during the execution of a specification. + *

+ * Depending on the context in which the error occurred, some of the methods may return {@code null}. + */ public interface IErrorContext { @Nullable SpecInfo getCurrentSpec(); diff --git a/spock-specs/src/test/groovy/org/spockframework/runtime/RunListenerSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/runtime/RunListenerSpec.groovy index 4e0736a03e..c9af3050d2 100644 --- a/spock-specs/src/test/groovy/org/spockframework/runtime/RunListenerSpec.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/runtime/RunListenerSpec.groovy @@ -234,6 +234,7 @@ class ASpec extends Specification { it.kind == block it.texts == blockTexts } + assert errorInfo.errorContext.toString() == '' } else { assert errorInfo.errorContext.currentBlock == null }