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
* 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
+ * 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
}