Skip to content

Commit 2c4e38c

Browse files
authored
Add BlockListener support... (#1575)
This feature allows extension authors to register an `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. Expose `IErrorContext` in `ErrorInfo` to provide more information in `IRunListener.error(ErrorInfo)` about where the error happened. fixes #538 fixes #111
1 parent 01f3c4e commit 2c4e38c

File tree

57 files changed

+1360
-125
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1360
-125
lines changed

docs/extensions.adoc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1380,3 +1380,26 @@ It is primarily for framework developers who want to provide a default value for
13801380
Or users of a framework that doesn't provide default values for their special types.
13811381

13821382
If you want to change the default response behavior for `Stub` have a look at <<interaction_based_testing.adoc#ALaCarteMocks,A la Carte Mocks>> and how to use your own `org.spockframework.mock.IDefaultResponse`.
1383+
1384+
=== Listeners
1385+
1386+
Extensions can register listeners to receive notifications about the progress of the test run.
1387+
These listeners are intended to be used for reporting, logging, or other monitoring purposes.
1388+
They are not intended to modify the test run in any way.
1389+
You can register the same listener instance on multiple specifications or features.
1390+
Please consult the JavaDoc of the respective listener interfaces for more information.
1391+
1392+
==== `IRunListener`
1393+
1394+
The `org.spockframework.runtime.IRunListener` can be registered via `SpecInfo.addListener(IRunListener)` and will receive notifications about the progress of the test run of a single specification.
1395+
1396+
[#block-listener]
1397+
==== `IBlockListener`
1398+
1399+
The `org.spockframework.runtime.extension.IBlockListener` can be registered on a feature via, `FeatureInfo.addBlockListener(IBlockListener)` and will receive notifications about the progress of the feature.
1400+
1401+
It will be called once when entering a block (`blockEntered`) and once when exiting a block (`blockExited`).
1402+
1403+
When an exception is thrown in a block, the `blockExited` will not be called for that block.
1404+
The failed block will be part of the `ErrorContext` in `ErrorInfo` that is passed to `IRunListener.error(ErrorInfo)`.
1405+
If a `cleanup` block is present the cleanup block listener methods will still be called.

docs/release_notes.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ include::include.adoc[]
99

1010
* Add support for combining two or more data providers using cartesian product spockIssue:1062[]
1111
* Add support for a `filter` block after a `where` block to filter out unwanted iterations spockPull:1927[]
12+
* Add <<extensions.adoc#block-listener,`IBlockListener`>> extension point to listen to block execution events within feature methods spockPull:1575[]
1213

1314
=== Misc
1415

spock-core/src/main/java/org/spockframework/compiler/AstNodeCache.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public class AstNodeCache {
4646
public final ClassNode SpecificationContext = ClassHelper.makeWithoutCaching(SpecificationContext.class);
4747
public final ClassNode DataVariableMultiplication = ClassHelper.makeWithoutCaching(DataVariableMultiplication.class);
4848
public final ClassNode DataVariableMultiplicationFactor = ClassHelper.makeWithoutCaching(DataVariableMultiplicationFactor.class);
49+
public final ClassNode BlockInfo = ClassHelper.makeWithoutCaching(BlockInfo.class);
4950

5051
public final MethodNode SpecInternals_GetSpecificationContext =
5152
SpecInternals.getDeclaredMethods(Identifiers.GET_SPECIFICATION_CONTEXT).get(0);
@@ -71,6 +72,12 @@ public class AstNodeCache {
7172
public final MethodNode SpockRuntime_DespreadList =
7273
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.DESPREAD_LIST).get(0);
7374

75+
public final MethodNode SpockRuntime_CallBlockEntered =
76+
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CALL_BLOCK_ENTERED).get(0);
77+
78+
public final MethodNode SpockRuntime_CallBlockExited =
79+
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CALL_BLOCK_EXITED).get(0);
80+
7481
public final MethodNode ValueRecorder_Reset =
7582
ValueRecorder.getDeclaredMethods(org.spockframework.runtime.ValueRecorder.RESET).get(0);
7683

@@ -107,6 +114,12 @@ public class AstNodeCache {
107114
public final MethodNode SpecificationContext_GetSharedInstance =
108115
SpecificationContext.getDeclaredMethods(org.spockframework.runtime.SpecificationContext.GET_SHARED_INSTANCE).get(0);
109116

117+
public final MethodNode SpecificationContext_GetCurrentBlock =
118+
SpecificationContext.getDeclaredMethods(org.spockframework.runtime.SpecificationContext.GET_CURRENT_BLOCK).get(0);
119+
120+
public final MethodNode SpecificationContext_SetCurrentBlock =
121+
SpecificationContext.getDeclaredMethods(org.spockframework.runtime.SpecificationContext.SET_CURRENT_BLOCK).get(0);
122+
110123
public final MethodNode List_Get =
111124
ClassHelper.LIST_TYPE.getDeclaredMethods("get").get(0);
112125

spock-core/src/main/java/org/spockframework/compiler/AstUtil.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.spockframework.compiler;
1818

1919
import org.codehaus.groovy.syntax.Token;
20+
import org.codehaus.groovy.syntax.Types;
2021
import org.spockframework.lang.Wildcard;
2122
import org.spockframework.util.*;
2223
import spock.lang.Specification;
@@ -390,4 +391,12 @@ public static ConstantExpression primitiveConstExpression(int value) {
390391
public static ConstantExpression primitiveConstExpression(boolean value) {
391392
return new ConstantExpression(value, true);
392393
}
394+
395+
public static BinaryExpression createVariableIsNotNullExpression(VariableExpression var) {
396+
return new BinaryExpression(
397+
var,
398+
Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1),
399+
new ConstantExpression(null));
400+
}
401+
393402
}

spock-core/src/main/java/org/spockframework/compiler/SpecAnnotator.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import org.codehaus.groovy.ast.*;
3232
import org.codehaus.groovy.ast.expr.*;
33+
import org.spockframework.util.Assert;
3334

3435
import static java.util.stream.Collectors.*;
3536
import static org.spockframework.compiler.AstUtil.*;
@@ -190,8 +191,9 @@ private void addFeatureMetadata(FeatureMethod feature) {
190191
ann.setMember(FeatureMetadata.BLOCKS, blockAnnElems = new ListExpression());
191192

192193
ListExpression paramNames = new ListExpression();
193-
for (Parameter param : feature.getAst().getParameters())
194+
for (Parameter param : feature.getAst().getParameters()) {
194195
paramNames.addExpression(new ConstantExpression(param.getName()));
196+
}
195197
ann.setMember(FeatureMetadata.PARAMETER_NAMES, paramNames);
196198

197199
feature.getAst().addAnnotation(ann);
@@ -202,9 +204,13 @@ private void addBlockMetadata(Block block, BlockKind kind) {
202204
blockAnn.setMember(BlockMetadata.KIND, new PropertyExpression(
203205
new ClassExpression(nodeCache.BlockKind), kind.name()));
204206
ListExpression textExprs = new ListExpression();
205-
for (String text : block.getDescriptions())
207+
for (String text : block.getDescriptions()) {
206208
textExprs.addExpression(new ConstantExpression(text));
209+
}
207210
blockAnn.setMember(BlockMetadata.TEXTS, textExprs);
211+
int index = blockAnnElems.getExpressions().size();
212+
Assert.that(index == block.getBlockMetaDataIndex(),
213+
() -> kind + " block mismatch of index: " + index + ", block.getBlockMetaDataIndex(): " + block.getBlockMetaDataIndex());
208214
blockAnnElems.addExpression(new AnnotationConstantExpression(blockAnn));
209215
}
210216

spock-core/src/main/java/org/spockframework/compiler/SpecParser.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,12 @@ private void buildBlocks(Method method) throws InvalidSpecCompileException {
200200
checkIsValidSuccessor(method, BlockParseInfo.METHOD_END,
201201
method.getAst().getLastLineNumber(), method.getAst().getLastColumnNumber());
202202

203+
// set the block metadata index for each block this must be equal to the index of the block in the @BlockMetadata annotation
204+
int i = -1;
205+
for (Block block : method.getBlocks()) {
206+
if(!block.hasBlockMetadata()) continue;
207+
block.setBlockMetaDataIndex(++i);
208+
}
203209
// now that statements have been copied to blocks, the original statement
204210
// list is cleared; statements will be copied back after rewriting is done
205211
stats.clear();

spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@
1616

1717
package org.spockframework.compiler;
1818

19-
import org.spockframework.compiler.model.*;
20-
import org.spockframework.runtime.SpockException;
21-
import org.spockframework.util.*;
22-
23-
import java.lang.reflect.InvocationTargetException;
24-
import java.util.*;
25-
2619
import org.codehaus.groovy.ast.*;
2720
import org.codehaus.groovy.ast.expr.*;
2821
import org.codehaus.groovy.ast.stmt.*;
2922
import org.codehaus.groovy.runtime.MetaClassHelper;
30-
import org.codehaus.groovy.syntax.*;
23+
import org.codehaus.groovy.syntax.Token;
24+
import org.codehaus.groovy.syntax.Types;
25+
import org.jetbrains.annotations.NotNull;
3126
import org.objectweb.asm.Opcodes;
27+
import org.spockframework.compiler.model.*;
28+
import org.spockframework.runtime.SpockException;
29+
import org.spockframework.util.InternalIdentifiers;
30+
import org.spockframework.util.ObjectUtil;
31+
import org.spockframework.util.ReflectionUtil;
32+
33+
import java.lang.reflect.InvocationTargetException;
34+
import java.util.*;
3235

3336
import static java.util.Arrays.asList;
3437
import static java.util.Collections.singletonList;
@@ -159,7 +162,7 @@ private void createFinalFieldGetter(Field field) {
159162

160163
private void createSharedFieldSetter(Field field) {
161164
String setterName = "set" + MetaClassHelper.capitalize(field.getName());
162-
Parameter[] params = new Parameter[] { new Parameter(field.getAst().getType(), "$spock_value") };
165+
Parameter[] params = new Parameter[] { new Parameter(field.getAst().getType(), SpockNames.SPOCK_VALUE) };
163166
MethodNode setter = spec.getAst().getMethod(setterName, params);
164167
if (setter != null) {
165168
errorReporter.error(field.getAst(),
@@ -180,7 +183,7 @@ private void createSharedFieldSetter(Field field) {
180183
// use internal name
181184
new ConstantExpression(field.getAst().getName())),
182185
Token.newSymbol(Types.ASSIGN, -1, -1),
183-
new VariableExpression("$spock_value"))));
186+
new VariableExpression(SpockNames.SPOCK_VALUE))));
184187

185188
setter.setSourcePosition(field.getAst());
186189
spec.getAst().addMethod(setter);
@@ -390,13 +393,20 @@ private void handleWhereBlock(Method method) {
390393
public void visitMethodAgain(Method method) {
391394
this.block = null;
392395

393-
if (!movedStatsBackToMethod)
394-
for (Block b : method.getBlocks())
396+
if (!movedStatsBackToMethod) {
397+
for (Block b : method.getBlocks()) {
398+
// This will only run if there was no 'cleanup' block in the method.
399+
// Otherwise, the blocks have already been copied to try block by visitCleanupBlock.
400+
// We need to run as late as possible, so we'll have to do the handling here and in visitCleanupBlock.
401+
addBlockListeners(b);
395402
method.getStatements().addAll(b.getAst());
403+
}
404+
}
396405

397406
// for global required interactions
398-
if (method instanceof FeatureMethod)
407+
if (method instanceof FeatureMethod) {
399408
method.getStatements().add(createMockControllerCall(nodeCache.MockController_LeaveScope));
409+
}
400410

401411
if (methodHasCondition) {
402412
defineValueRecorder(method.getStatements(), "");
@@ -406,6 +416,56 @@ public void visitMethodAgain(Method method) {
406416
}
407417
}
408418

419+
420+
private void addBlockListeners(Block block) {
421+
BlockParseInfo blockType = block.getParseInfo();
422+
if (!blockType.isSupportingBlockListeners()) return;
423+
424+
// SpockRuntime.callBlockEntered(getSpecificationContext(), blockMetadataIndex)
425+
MethodCallExpression blockEnteredCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallBlockEntered);
426+
// SpockRuntime.callBlockExited(getSpecificationContext(), blockMetadataIndex)
427+
MethodCallExpression blockExitedCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallBlockExited);
428+
429+
block.getAst().add(0, new ExpressionStatement(blockEnteredCall));
430+
if (blockType == BlockParseInfo.CLEANUP) {
431+
// In case of a cleanup block we need store a reference of the previously `currentBlock` in case that an exception occurred
432+
// and restore it at the end of the cleanup block, so that the correct `BlockInfo` is available for the `IErrorContext`.
433+
// The restoration happens in the `finally` statement created by `createCleanupTryCatch`.
434+
VariableExpression failedBlock = new VariableExpression(SpockNames.FAILED_BLOCK, nodeCache.BlockInfo);
435+
block.getAst().add(0, ifThrowableIsNotNull(storeFailedBlock(failedBlock)));
436+
}
437+
block.getAst().add(new ExpressionStatement(blockExitedCall));
438+
}
439+
440+
private @NotNull Statement storeFailedBlock(VariableExpression failedBlock) {
441+
MethodCallExpression getCurrentBlock = createDirectMethodCall(getSpecificationContext(), nodeCache.SpecificationContext_GetCurrentBlock, ArgumentListExpression.EMPTY_ARGUMENTS);
442+
return new ExpressionStatement(new BinaryExpression(failedBlock, Token.newSymbol(Types.ASSIGN, -1, -1), getCurrentBlock));
443+
}
444+
445+
private @NotNull Statement restoreFailedBlock(VariableExpression failedBlock) {
446+
return new ExpressionStatement(createDirectMethodCall(new CastExpression(nodeCache.SpecificationContext, getSpecificationContext()), nodeCache.SpecificationContext_SetCurrentBlock, new ArgumentListExpression(failedBlock)));
447+
}
448+
449+
private IfStatement ifThrowableIsNotNull(Statement statement) {
450+
return new IfStatement(
451+
// if ($spock_feature_throwable != null)
452+
new BooleanExpression(AstUtil.createVariableIsNotNullExpression(new VariableExpression(SpockNames.SPOCK_FEATURE_THROWABLE, nodeCache.Throwable))),
453+
statement,
454+
EmptyStatement.INSTANCE
455+
);
456+
}
457+
458+
private MethodCallExpression createBlockListenerCall(Block block, BlockParseInfo blockType, MethodNode blockListenerMethod) {
459+
if (block.getBlockMetaDataIndex() < 0) throw new SpockException("Block metadata index not set: " + block);
460+
return createDirectMethodCall(
461+
new ClassExpression(nodeCache.SpockRuntime),
462+
blockListenerMethod,
463+
new ArgumentListExpression(
464+
getSpecificationContext(),
465+
new ConstantExpression(block.getBlockMetaDataIndex(), true)
466+
));
467+
}
468+
409469
@Override
410470
public void visitAnyBlock(Block block) {
411471
this.block = block;
@@ -484,12 +544,15 @@ private Statement createMockControllerCall(MethodNode method) {
484544
@Override
485545
public void visitCleanupBlock(CleanupBlock block) {
486546
for (Block b : method.getBlocks()) {
547+
// call addBlockListeners() here, as this method will already copy the contents of the blocks,
548+
// so we need to transform the block listeners here as they won't be copied in visitMethodAgain where we normally add them
549+
addBlockListeners(b);
487550
if (b == block) break;
488551
moveVariableDeclarations(b.getAst(), method.getStatements());
489552
}
490553

491554
VariableExpression featureThrowableVar =
492-
new VariableExpression("$spock_feature_throwable", nodeCache.Throwable);
555+
new VariableExpression(SpockNames.SPOCK_FEATURE_THROWABLE, nodeCache.Throwable);
493556
method.getStatements().add(createVariableDeclarationStatement(featureThrowableVar));
494557

495558
List<Statement> featureStats = new ArrayList<>();
@@ -499,9 +562,10 @@ public void visitCleanupBlock(CleanupBlock block) {
499562
}
500563

501564
CatchStatement featureCatchStat = createThrowableAssignmentAndRethrowCatchStatement(featureThrowableVar);
502-
503-
List<Statement> cleanupStats = singletonList(
504-
createCleanupTryCatch(block, featureThrowableVar));
565+
VariableExpression failedBlock = new VariableExpression(SpockNames.FAILED_BLOCK, nodeCache.BlockInfo);
566+
List<Statement> cleanupStats = asList(
567+
new ExpressionStatement(new DeclarationExpression(failedBlock, Token.newSymbol(Types.ASSIGN, -1, -1), ConstantExpression.NULL)),
568+
createCleanupTryCatch(block, featureThrowableVar, failedBlock));
505569

506570
TryCatchStatement tryFinally =
507571
new TryCatchStatement(
@@ -517,13 +581,6 @@ public void visitCleanupBlock(CleanupBlock block) {
517581
movedStatsBackToMethod = true;
518582
}
519583

520-
private BinaryExpression createVariableNotNullExpression(VariableExpression var) {
521-
return new BinaryExpression(
522-
new VariableExpression(var),
523-
Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1),
524-
new ConstantExpression(null));
525-
}
526-
527584
private Statement createVariableDeclarationStatement(VariableExpression var) {
528585
DeclarationExpression throwableDecl =
529586
new DeclarationExpression(
@@ -534,21 +591,21 @@ private Statement createVariableDeclarationStatement(VariableExpression var) {
534591
return new ExpressionStatement(throwableDecl);
535592
}
536593

537-
private TryCatchStatement createCleanupTryCatch(CleanupBlock block, VariableExpression featureThrowableVar) {
594+
private TryCatchStatement createCleanupTryCatch(CleanupBlock block, VariableExpression featureThrowableVar, VariableExpression failedBlock) {
538595
List<Statement> cleanupStats = new ArrayList<>(block.getAst());
539-
540596
TryCatchStatement tryCatchStat =
541597
new TryCatchStatement(
542598
new BlockStatement(cleanupStats, null),
543-
EmptyStatement.INSTANCE);
599+
ifThrowableIsNotNull(restoreFailedBlock(failedBlock))
600+
);
544601

545602
tryCatchStat.addCatch(createHandleSuppressedThrowableStatement(featureThrowableVar));
546603

547604
return tryCatchStat;
548605
}
549606

550607
private CatchStatement createThrowableAssignmentAndRethrowCatchStatement(VariableExpression assignmentVar) {
551-
Parameter catchParameter = new Parameter(nodeCache.Throwable, "$spock_tmp_throwable");
608+
Parameter catchParameter = new Parameter(nodeCache.Throwable, SpockNames.SPOCK_TMP_THROWABLE);
552609

553610
BinaryExpression assignThrowableExpr =
554611
new BinaryExpression(
@@ -565,9 +622,9 @@ private CatchStatement createThrowableAssignmentAndRethrowCatchStatement(Variabl
565622
}
566623

567624
private CatchStatement createHandleSuppressedThrowableStatement(VariableExpression featureThrowableVar) {
568-
Parameter catchParameter = new Parameter(nodeCache.Throwable, "$spock_tmp_throwable");
625+
Parameter catchParameter = new Parameter(nodeCache.Throwable, SpockNames.SPOCK_TMP_THROWABLE);
569626

570-
BinaryExpression featureThrowableNotNullExpr = createVariableNotNullExpression(featureThrowableVar);
627+
BinaryExpression featureThrowableNotNullExpr = AstUtil.createVariableIsNotNullExpression(featureThrowableVar);
571628

572629
List<Statement> addSuppressedStats =
573630
singletonList(new ExpressionStatement(
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package org.spockframework.compiler;
22

33
public class SpockNames {
4-
public static final String VALUE_RECORDER = "$spock_valueRecorder";
54
public static final String ERROR_COLLECTOR = "$spock_errorCollector";
5+
public static final String FAILED_BLOCK = "$spock_failedBlock";
66
public static final String OLD_VALUE = "$spock_oldValue";
77
public static final String SPOCK_EX = "$spock_ex";
8+
public static final String SPOCK_FEATURE_THROWABLE = "$spock_feature_throwable";
9+
public static final String SPOCK_TMP_THROWABLE = "$spock_tmp_throwable";
10+
public static final String SPOCK_VALUE = "$spock_value";
11+
public static final String VALUE_RECORDER = "$spock_valueRecorder";
812
}

spock-core/src/main/java/org/spockframework/compiler/model/AnonymousBlock.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ public void accept(ISpecVisitor visitor) throws Exception {
3737
public BlockParseInfo getParseInfo() {
3838
return BlockParseInfo.ANONYMOUS;
3939
}
40+
41+
@Override
42+
public boolean hasBlockMetadata() {
43+
return false;
44+
}
4045
}

0 commit comments

Comments
 (0)