Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BlockListener support... #1575

Merged
merged 27 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e95a3db
Add BlockListener support...
leonard84 Dec 21, 2022
de2bff0
Expose IErrorContext in ErrorInfo...
leonard84 Dec 22, 2022
d77ab6c
Clear current block on exit from feature method
leonard84 Feb 16, 2023
a231f04
Update AST tests with block listeners
leonard84 Feb 16, 2023
3407a1a
Add blockExited to IBlockListener
leonard84 Mar 10, 2023
df354af
Add interaction example AstSpec
leonard84 Mar 10, 2023
ffe6edc
Fix block listeners for interactions
leonard84 Mar 29, 2023
7f68336
Implement Review Feedback
leonard84 Mar 21, 2024
f0e69a7
Fix test in RunListenerSpec
leonard84 Mar 23, 2024
802b84e
Implement Review comments
leonard84 Mar 23, 2024
5a5cdfd
Move current block clearing to PlatformSpecRunner
leonard84 Apr 28, 2024
146689f
Move DataProviders expected ast to snapshots
leonard84 Apr 28, 2024
fa4811b
Restore failed block when cleanup runs
leonard84 May 14, 2024
1f6aa2d
Implement Review comments re SpecificationContext
leonard84 May 23, 2024
bbba7f5
Implement Review comments re ErrorContext/Info
leonard84 May 23, 2024
49dfa26
Revert all changes to DeepBlockRewriter
leonard84 May 31, 2024
4f4e4c5
Implement Review Comments
leonard84 May 31, 2024
3f8f22a
Use BlockInfo from SpecInfo via an index lookup
leonard84 Jun 23, 2024
c6d4b01
Polishing
leonard84 Oct 18, 2024
acc8319
Apply review comments
leonard84 Oct 18, 2024
cf8b43a
Add minimal documentation
leonard84 Nov 1, 2024
ac96ff4
Apply review comments
leonard84 Dec 18, 2024
379aefc
Apply review comments 2
leonard84 Dec 18, 2024
a6bce08
Apply review comments 3
leonard84 Dec 18, 2024
a04dfae
Update remaining snapshots
leonard84 Dec 18, 2024
19e2dc2
Implement review feedback
leonard84 Dec 26, 2024
a8347ad
Merge branch 'master' into add-block-listener-support
leonard84 Dec 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1380,3 +1380,26 @@ It is primarily for framework developers who want to provide a default value for
Or users of a framework that doesn't provide default values for their special types.

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`.

=== Listeners

Extensions can register listeners to receive notifications about the progress of the test run.
These listeners are intended to be used for reporting, logging, or other monitoring purposes.
They are not intended to modify the test run in any way.
You can register the same listener instance on multiple specifications or features.
Please consult the JavaDoc of the respective listener interfaces for more information.

==== `IRunListener`

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.

[#block-listener]
==== `IBlockListener`

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.

It will be called once when entering a block (`blockEntered`) and once when exiting a block (`blockExited`).

When an exception is thrown in a block, the `blockExited` will not be called for that block.
The failed block will be part of the `ErrorContext` in `ErrorInfo` that is passed to `IRunListener.error(ErrorInfo)`.
If a `cleanup` block is present the cleanup block listener methods will still be called.
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ include::include.adoc[]

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

=== Misc

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class AstNodeCache {
public final ClassNode SpecificationContext = ClassHelper.makeWithoutCaching(SpecificationContext.class);
public final ClassNode DataVariableMultiplication = ClassHelper.makeWithoutCaching(DataVariableMultiplication.class);
public final ClassNode DataVariableMultiplicationFactor = ClassHelper.makeWithoutCaching(DataVariableMultiplicationFactor.class);
public final ClassNode BlockInfo = ClassHelper.makeWithoutCaching(BlockInfo.class);

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

public final MethodNode SpockRuntime_CallBlockEntered =
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CALL_BLOCK_ENTERED).get(0);

public final MethodNode SpockRuntime_CallBlockExited =
SpockRuntime.getDeclaredMethods(org.spockframework.runtime.SpockRuntime.CALL_BLOCK_EXITED).get(0);

public final MethodNode ValueRecorder_Reset =
ValueRecorder.getDeclaredMethods(org.spockframework.runtime.ValueRecorder.RESET).get(0);

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

public final MethodNode SpecificationContext_GetCurrentBlock =
SpecificationContext.getDeclaredMethods(org.spockframework.runtime.SpecificationContext.GET_CURRENT_BLOCK).get(0);

public final MethodNode SpecificationContext_SetCurrentBlock =
SpecificationContext.getDeclaredMethods(org.spockframework.runtime.SpecificationContext.SET_CURRENT_BLOCK).get(0);

public final MethodNode List_Get =
ClassHelper.LIST_TYPE.getDeclaredMethods("get").get(0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.spockframework.compiler;

import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.spockframework.lang.Wildcard;
import org.spockframework.util.*;
import spock.lang.Specification;
Expand Down Expand Up @@ -390,4 +391,12 @@ public static ConstantExpression primitiveConstExpression(int value) {
public static ConstantExpression primitiveConstExpression(boolean value) {
return new ConstantExpression(value, true);
}

public static BinaryExpression createVariableIsNotNullExpression(VariableExpression var) {
return new BinaryExpression(
var,
Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1),
new ConstantExpression(null));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.spockframework.util.Assert;

import static java.util.stream.Collectors.*;
import static org.spockframework.compiler.AstUtil.*;
Expand Down Expand Up @@ -190,8 +191,9 @@
ann.setMember(FeatureMetadata.BLOCKS, blockAnnElems = new ListExpression());

ListExpression paramNames = new ListExpression();
for (Parameter param : feature.getAst().getParameters())
for (Parameter param : feature.getAst().getParameters()) {
paramNames.addExpression(new ConstantExpression(param.getName()));
}
ann.setMember(FeatureMetadata.PARAMETER_NAMES, paramNames);

feature.getAst().addAnnotation(ann);
Expand All @@ -202,9 +204,13 @@
blockAnn.setMember(BlockMetadata.KIND, new PropertyExpression(
new ClassExpression(nodeCache.BlockKind), kind.name()));
ListExpression textExprs = new ListExpression();
for (String text : block.getDescriptions())
for (String text : block.getDescriptions()) {
textExprs.addExpression(new ConstantExpression(text));
}
blockAnn.setMember(BlockMetadata.TEXTS, textExprs);
int index = blockAnnElems.getExpressions().size();
Assert.that(index == block.getBlockMetaDataIndex(),
() -> kind + " block mismatch of index: " + index + ", block.getBlockMetaDataIndex(): " + block.getBlockMetaDataIndex());

Check warning on line 213 in spock-core/src/main/java/org/spockframework/compiler/SpecAnnotator.java

View check run for this annotation

Codecov / codecov/patch

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

Added line #L213 was not covered by tests
blockAnnElems.addExpression(new AnnotationConstantExpression(blockAnn));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ private void buildBlocks(Method method) throws InvalidSpecCompileException {
checkIsValidSuccessor(method, BlockParseInfo.METHOD_END,
method.getAst().getLastLineNumber(), method.getAst().getLastColumnNumber());

// set the block metadata index for each block this must be equal to the index of the block in the @BlockMetadata annotation
int i = -1;
for (Block block : method.getBlocks()) {
if(!block.hasBlockMetadata()) continue;
block.setBlockMetaDataIndex(++i);
}
// now that statements have been copied to blocks, the original statement
// list is cleared; statements will be copied back after rewriting is done
stats.clear();
Expand Down
117 changes: 87 additions & 30 deletions spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@

package org.spockframework.compiler;

import org.spockframework.compiler.model.*;
import org.spockframework.runtime.SpockException;
import org.spockframework.util.*;

import java.lang.reflect.InvocationTargetException;
import java.util.*;

import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
import org.codehaus.groovy.runtime.MetaClassHelper;
import org.codehaus.groovy.syntax.*;
import org.codehaus.groovy.syntax.Token;
import org.codehaus.groovy.syntax.Types;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.Opcodes;
import org.spockframework.compiler.model.*;
import org.spockframework.runtime.SpockException;
import org.spockframework.util.InternalIdentifiers;
import org.spockframework.util.ObjectUtil;
import org.spockframework.util.ReflectionUtil;

import java.lang.reflect.InvocationTargetException;
import java.util.*;

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

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

setter.setSourcePosition(field.getAst());
spec.getAst().addMethod(setter);
Expand Down Expand Up @@ -390,13 +393,20 @@ private void handleWhereBlock(Method method) {
public void visitMethodAgain(Method method) {
this.block = null;

if (!movedStatsBackToMethod)
for (Block b : method.getBlocks())
if (!movedStatsBackToMethod) {
for (Block b : method.getBlocks()) {
// 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());
}
leonard84 marked this conversation as resolved.
Show resolved Hide resolved
}

// for global required interactions
if (method instanceof FeatureMethod)
if (method instanceof FeatureMethod) {
method.getStatements().add(createMockControllerCall(nodeCache.MockController_LeaveScope));
}

if (methodHasCondition) {
defineValueRecorder(method.getStatements(), "");
Expand All @@ -406,6 +416,56 @@ public void visitMethodAgain(Method method) {
}
}


private void addBlockListeners(Block block) {
BlockParseInfo blockType = block.getParseInfo();
if (!blockType.isSupportingBlockListeners()) return;

// SpockRuntime.callBlockEntered(getSpecificationContext(), blockMetadataIndex)
MethodCallExpression blockEnteredCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallBlockEntered);
// SpockRuntime.callBlockExited(getSpecificationContext(), blockMetadataIndex)
MethodCallExpression blockExitedCall = createBlockListenerCall(block, blockType, nodeCache.SpockRuntime_CallBlockExited);

block.getAst().add(0, new ExpressionStatement(blockEnteredCall));
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().add(0, ifThrowableIsNotNull(storeFailedBlock(failedBlock)));
}
block.getAst().add(new ExpressionStatement(blockExitedCall));
}

private @NotNull Statement storeFailedBlock(VariableExpression failedBlock) {
MethodCallExpression getCurrentBlock = createDirectMethodCall(getSpecificationContext(), nodeCache.SpecificationContext_GetCurrentBlock, ArgumentListExpression.EMPTY_ARGUMENTS);
return new ExpressionStatement(new BinaryExpression(failedBlock, Token.newSymbol(Types.ASSIGN, -1, -1), getCurrentBlock));
}

private @NotNull Statement restoreFailedBlock(VariableExpression failedBlock) {
return new ExpressionStatement(createDirectMethodCall(new CastExpression(nodeCache.SpecificationContext, getSpecificationContext()), nodeCache.SpecificationContext_SetCurrentBlock, new ArgumentListExpression(failedBlock)));
}

private IfStatement ifThrowableIsNotNull(Statement statement) {
return new IfStatement(
// if ($spock_feature_throwable != null)
new BooleanExpression(AstUtil.createVariableIsNotNullExpression(new VariableExpression(SpockNames.SPOCK_FEATURE_THROWABLE, nodeCache.Throwable))),
statement,
EmptyStatement.INSTANCE
);
}

private MethodCallExpression createBlockListenerCall(Block block, BlockParseInfo blockType, MethodNode blockListenerMethod) {
if (block.getBlockMetaDataIndex() < 0) throw new SpockException("Block metadata index not set: " + block);
return createDirectMethodCall(
new ClassExpression(nodeCache.SpockRuntime),
blockListenerMethod,
new ArgumentListExpression(
getSpecificationContext(),
new ConstantExpression(block.getBlockMetaDataIndex(), true)
));
}

@Override
public void visitAnyBlock(Block block) {
this.block = block;
Expand Down Expand Up @@ -484,12 +544,15 @@ private Statement createMockControllerCall(MethodNode method) {
@Override
public void visitCleanupBlock(CleanupBlock block) {
for (Block b : method.getBlocks()) {
// call addBlockListeners() here, as this method will already copy the contents of the blocks,
// so we need to transform the block listeners here as they won't be copied in visitMethodAgain where we normally add them
addBlockListeners(b);
if (b == block) break;
moveVariableDeclarations(b.getAst(), method.getStatements());
}

VariableExpression featureThrowableVar =
new VariableExpression("$spock_feature_throwable", nodeCache.Throwable);
new VariableExpression(SpockNames.SPOCK_FEATURE_THROWABLE, nodeCache.Throwable);
method.getStatements().add(createVariableDeclarationStatement(featureThrowableVar));

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

CatchStatement featureCatchStat = createThrowableAssignmentAndRethrowCatchStatement(featureThrowableVar);

List<Statement> cleanupStats = singletonList(
createCleanupTryCatch(block, featureThrowableVar));
VariableExpression failedBlock = new VariableExpression(SpockNames.FAILED_BLOCK, nodeCache.BlockInfo);
List<Statement> cleanupStats = asList(
new ExpressionStatement(new DeclarationExpression(failedBlock, Token.newSymbol(Types.ASSIGN, -1, -1), ConstantExpression.NULL)),
createCleanupTryCatch(block, featureThrowableVar, failedBlock));

TryCatchStatement tryFinally =
new TryCatchStatement(
Expand All @@ -517,13 +581,6 @@ public void visitCleanupBlock(CleanupBlock block) {
movedStatsBackToMethod = true;
}

private BinaryExpression createVariableNotNullExpression(VariableExpression var) {
return new BinaryExpression(
new VariableExpression(var),
Token.newSymbol(Types.COMPARE_NOT_EQUAL, -1, -1),
new ConstantExpression(null));
}

private Statement createVariableDeclarationStatement(VariableExpression var) {
DeclarationExpression throwableDecl =
new DeclarationExpression(
Expand All @@ -534,21 +591,21 @@ private Statement createVariableDeclarationStatement(VariableExpression var) {
return new ExpressionStatement(throwableDecl);
}

private TryCatchStatement createCleanupTryCatch(CleanupBlock block, VariableExpression featureThrowableVar) {
private TryCatchStatement createCleanupTryCatch(CleanupBlock block, VariableExpression featureThrowableVar, VariableExpression failedBlock) {
List<Statement> cleanupStats = new ArrayList<>(block.getAst());

TryCatchStatement tryCatchStat =
new TryCatchStatement(
new BlockStatement(cleanupStats, null),
EmptyStatement.INSTANCE);
ifThrowableIsNotNull(restoreFailedBlock(failedBlock))
);

tryCatchStat.addCatch(createHandleSuppressedThrowableStatement(featureThrowableVar));

return tryCatchStat;
}

private CatchStatement createThrowableAssignmentAndRethrowCatchStatement(VariableExpression assignmentVar) {
Parameter catchParameter = new Parameter(nodeCache.Throwable, "$spock_tmp_throwable");
Parameter catchParameter = new Parameter(nodeCache.Throwable, SpockNames.SPOCK_TMP_THROWABLE);

BinaryExpression assignThrowableExpr =
new BinaryExpression(
Expand All @@ -565,9 +622,9 @@ private CatchStatement createThrowableAssignmentAndRethrowCatchStatement(Variabl
}

private CatchStatement createHandleSuppressedThrowableStatement(VariableExpression featureThrowableVar) {
Parameter catchParameter = new Parameter(nodeCache.Throwable, "$spock_tmp_throwable");
Parameter catchParameter = new Parameter(nodeCache.Throwable, SpockNames.SPOCK_TMP_THROWABLE);

BinaryExpression featureThrowableNotNullExpr = createVariableNotNullExpression(featureThrowableVar);
BinaryExpression featureThrowableNotNullExpr = AstUtil.createVariableIsNotNullExpression(featureThrowableVar);

List<Statement> addSuppressedStats =
singletonList(new ExpressionStatement(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.spockframework.compiler;

public class SpockNames {
public static final String VALUE_RECORDER = "$spock_valueRecorder";
public static final String ERROR_COLLECTOR = "$spock_errorCollector";
public static final String FAILED_BLOCK = "$spock_failedBlock";
public static final String OLD_VALUE = "$spock_oldValue";
public static final String SPOCK_EX = "$spock_ex";
public static final String SPOCK_FEATURE_THROWABLE = "$spock_feature_throwable";
public static final String SPOCK_TMP_THROWABLE = "$spock_tmp_throwable";
public static final String SPOCK_VALUE = "$spock_value";
public static final String VALUE_RECORDER = "$spock_valueRecorder";
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ public void accept(ISpecVisitor visitor) throws Exception {
public BlockParseInfo getParseInfo() {
return BlockParseInfo.ANONYMOUS;
}

@Override
public boolean hasBlockMetadata() {
return false;
}
}
Loading
Loading