Skip to content

Commit

Permalink
Add BlockListener support...
Browse files Browse the repository at this point in the history
This feature allows extension authors to register a IBlockListener for
a feature to observe the execution of a feature in more detail.
This surfaces some of Spock's idiosyncrasies, for example interaction
assertions are actually setup right before entering the preceding
`when`-block as well as being evaluated on leaving the `when`-block
before actually entering the `then`-block.

The only valid block description is a constant String, although some
users mistakenly try to use a dynamic GString. Using anything other
than a String, will be treated as a separate statement and thus ignored.
  • Loading branch information
leonard84 committed Feb 16, 2023
1 parent 630e22a commit bbdc5d9
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class AstNodeCache {
public final ClassNode SpecInternals = ClassHelper.makeWithoutCaching(SpecInternals.class);
public final ClassNode MockController = ClassHelper.makeWithoutCaching(MockController.class);
public final ClassNode SpecificationContext = ClassHelper.makeWithoutCaching(SpecificationContext.class);
public final ClassNode BlockInfo = ClassHelper.makeWithoutCaching(BlockInfo.class);

public final MethodNode SpecInternals_GetSpecificationContext =
SpecInternals.getDeclaredMethods(Identifiers.GET_SPECIFICATION_CONTEXT).get(0);
Expand All @@ -68,6 +69,8 @@ public class AstNodeCache {

public final MethodNode SpockRuntime_DespreadList =
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.DESPREAD_LIST).get(0);
public final MethodNode SpockRuntime_CallEnterBlock =
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CALL_ENTER_BLOCK).get(0);

public final MethodNode ValueRecorder_Reset =
ValueRecorder.getDeclaredMethods(org.spockframework.runtime.ValueRecorder.RESET).get(0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@
import org.spockframework.util.*;

import java.util.*;
import java.util.stream.Collectors;

import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.syntax.Types;

import static java.util.Arrays.asList;
import static org.codehaus.groovy.ast.expr.MethodCallExpression.NO_ARGUMENTS;
import static org.spockframework.compiler.AstUtil.createDirectMethodCall;

/**
* Walks the statement and expression tree to:
Expand All @@ -54,6 +55,34 @@ public DeepBlockRewriter(IRewriteResources resources) {
@Override
public void visit(Block block) {
super.visit(block);
addBlockEnterCall(block);
}

private void addBlockEnterCall(Block block) {
BlockParseInfo blockType = block.getParseInfo();
if (blockType == BlockParseInfo.WHERE
|| blockType == BlockParseInfo.METHOD_END
|| blockType == BlockParseInfo.ANONYMOUS) return;

MethodCallExpression enterBlockCall = createDirectMethodCall(
new ClassExpression(resources.getAstNodeCache().SpockRuntime),
resources.getAstNodeCache().SpockRuntime_CallEnterBlock,
new ArgumentListExpression(
createDirectMethodCall(VariableExpression.THIS_EXPRESSION,
resources.getAstNodeCache().SpecInternals_GetSpecificationContext,
ArgumentListExpression.EMPTY_ARGUMENTS),
new ConstructorCallExpression(resources.getAstNodeCache().BlockInfo,
new ArgumentListExpression(
new PropertyExpression(
new ClassExpression(resources.getAstNodeCache().BlockKind),
blockType.name()
),
new ListExpression(
block.getDescriptions().stream().map(ConstantExpression::new).collect(Collectors.toList())
)
))
));
block.getAst().add(0, new ExpressionStatement(enterBlockCall));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

package org.spockframework.lang;

import org.spockframework.runtime.model.BlockInfo;
import org.spockframework.runtime.model.FeatureInfo;
import org.spockframework.runtime.model.SpecInfo;
import org.spockframework.util.Beta;
Expand All @@ -23,9 +24,14 @@

@Beta
public interface ISpecificationContext {
@Nullable
SpecInfo getCurrentSpec();
@Nullable
FeatureInfo getCurrentFeature();
@Nullable
IterationInfo getCurrentIteration();
@Nullable
BlockInfo getCurrentBlock();

@Nullable
Throwable getThrownException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class SpecificationContext implements ISpecificationContext {
private volatile SpecInfo currentSpec;
private volatile IterationInfo currentIteration;

private volatile BlockInfo currentBlock;

private volatile Specification sharedInstance;

private volatile Throwable thrownException;
Expand Down Expand Up @@ -50,6 +52,15 @@ public IterationInfo getCurrentIteration() {
return currentIteration;
}

void setCurrentBlock(BlockInfo blockInfo) {
this.currentBlock = blockInfo;
}

@Override
public BlockInfo getCurrentBlock() {
return currentBlock;
}

public void setCurrentIteration(IterationInfo currentIteration) {
this.currentIteration = currentIteration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static java.util.stream.Collectors.toList;

import org.spockframework.runtime.extension.IBlockListener;
import org.spockframework.runtime.model.*;
import org.spockframework.util.*;

Expand Down Expand Up @@ -146,6 +147,17 @@ public static Object[] despreadList(Object[] args, Object[] spreads, int[] posit
return GroovyRuntimeUtil.despreadList(args, spreads, positions);
}

public static final String CALL_ENTER_BLOCK = "callEnterBlock";
public static void callEnterBlock(SpecificationContext context, BlockInfo blockInfo) {
IterationInfo currentIteration = context.getCurrentIteration();
context.setCurrentBlock(blockInfo);
List<IBlockListener> blockListeners = currentIteration.getFeature().getBlockListeners();
if (blockListeners.isEmpty()) return;
for (IBlockListener blockListener : blockListeners) {
blockListener.blockEntered(currentIteration, blockInfo);
}
}

private static List<Object> getValues(ValueRecorder recorder) {
return recorder == null ? null : recorder.getValues();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.spockframework.runtime.extension;

import org.spockframework.runtime.model.BlockInfo;
import org.spockframework.runtime.model.IterationInfo;

public interface IBlockListener {
void blockEntered(IterationInfo iterationInfo, BlockInfo blockInfo);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ public class BlockInfo {
private BlockKind kind;
private List<String> texts;

public BlockInfo() {
}

public BlockInfo(BlockKind kind, List<String> texts) {
this.kind = kind;
this.texts = texts;
}

public BlockKind getKind() {
return kind;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.spockframework.runtime.model;

import org.spockframework.runtime.extension.IBlockListener;
import org.spockframework.runtime.extension.IDataDriver;
import org.spockframework.runtime.extension.IMethodInterceptor;
import org.spockframework.runtime.model.parallel.*;
Expand All @@ -21,6 +22,8 @@ public class FeatureInfo extends SpecElementInfo<SpecInfo, AnnotatedElement> imp
private final List<BlockInfo> blocks = new ArrayList<>();
private final List<IMethodInterceptor> iterationInterceptors = new ArrayList<>();

private final List<IBlockListener> blockListeners = new ArrayList<>();

private final Set<ExclusiveResource> exclusiveResources = new HashSet<>();

private final Set<TestTag> testTags = new HashSet<>();
Expand Down Expand Up @@ -95,6 +98,14 @@ public void addIterationInterceptor(IMethodInterceptor interceptor) {
iterationInterceptors.add(interceptor);
}

public List<IBlockListener> getBlockListeners() {
return blockListeners;
}

public void addBlockListener(IBlockListener blockListener) {
blockListeners.add(blockListener);
}

public MethodInfo getFeatureMethod() {
return featureMethod;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.spockframework.runtime

import org.spockframework.runtime.model.BlockInfo
import org.spockframework.runtime.model.BlockKind
import org.spockframework.runtime.model.IterationInfo
import spock.lang.Specification

class BlockListenerSpec extends Specification {

List<BlockInfo> blocks = []

def setup() {
specificationContext.currentIteration.feature.addBlockListener { IterationInfo i, BlockInfo b ->
blocks << b
}
}

def "BlockListener is called for each Block with text"() {
given: "setup"
expect: "precondition"
when: "action"
then: "assertion"

cleanup: "cleanup"
assert blocks.kind == [BlockKind.SETUP, BlockKind.EXPECT, BlockKind.WHEN, BlockKind.THEN, BlockKind.CLEANUP]
assert blocks.texts == [["setup"], ["precondition"], ["action"], ["assertion"], ["cleanup"]]
}

def "SpecificationContext holds a reference to the current block"() {
assert specificationContext.currentBlock == null
given: "setup"
assert specificationContext.currentBlock.kind == BlockKind.SETUP
expect: "precondition"
specificationContext.currentBlock.kind == BlockKind.EXPECT
when: "action"
assert specificationContext.currentBlock.kind == BlockKind.WHEN
then: "assertion"
specificationContext.currentBlock.kind == BlockKind.THEN

cleanup: "cleanup"
assert specificationContext.currentBlock.kind == BlockKind.CLEANUP
}

def "blocks extended with and: are treated as singular block with multiple texts"() {
given: "setup"
and: "setup2"
expect: "precondition"
and: "precondition2"
when: "action"
and: "action2"
then: "assertion"
and: "assertion2"

cleanup: "cleanup"
assert blocks.kind == [BlockKind.SETUP, BlockKind.EXPECT, BlockKind.WHEN, BlockKind.THEN, BlockKind.CLEANUP]
and: "cleanup2"
assert blocks.texts == [["setup", "setup2"], ["precondition", "precondition2"], ["action", "action2"], ["assertion", "assertion2"], ["cleanup", "cleanup2"]]
}
}

0 comments on commit bbdc5d9

Please sign in to comment.