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

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

leonard84
Copy link
Member

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.

fixes #538
fixes #111

@codecov
Copy link

codecov bot commented Feb 16, 2023

Codecov Report

Attention: Patch coverage is 79.86111% with 29 lines in your changes missing coverage. Please review.

Project coverage is 81.86%. Comparing base (2c7db77) to head (bd9e694).
Report is 151 commits behind head on master.

Files with missing lines Patch % Lines
...rg/spockframework/runtime/DataIteratorFactory.java 40.00% 9 Missing ⚠️
.../java/org/spockframework/runtime/ErrorContext.java 63.15% 7 Missing ⚠️
...main/java/org/spockframework/compiler/AstUtil.java 50.00% 2 Missing ⚠️
...ava/org/spockframework/compiler/SpecAnnotator.java 66.66% 1 Missing and 1 partial ⚠️
...java/org/spockframework/compiler/SpecRewriter.java 94.59% 0 Missing and 2 partials ⚠️
...ockframework/runtime/extension/IBlockListener.java 0.00% 2 Missing ⚠️
...va/org/spockframework/runtime/model/BlockInfo.java 66.66% 2 Missing ⚠️
.../java/org/spockframework/compiler/model/Block.java 83.33% 1 Missing ⚠️
...org/spockframework/runtime/PlatformSpecRunner.java 75.00% 1 Missing ⚠️
...va/org/spockframework/runtime/model/ErrorInfo.java 75.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #1575      +/-   ##
============================================
+ Coverage     80.44%   81.86%   +1.41%     
- Complexity     4337     4639     +302     
============================================
  Files           441      450       +9     
  Lines         13534    14550    +1016     
  Branches       1707     1834     +127     
============================================
+ Hits          10888    11911    +1023     
+ Misses         2008     1958      -50     
- Partials        638      681      +43     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@kriegaex
Copy link
Contributor

Hi @leonard84. I cannot perform a formal code review, because I am not a committer, but I hope that during the weekend I can find a small time slice to build and play around with it. I simply wanted to say thanks in advance for taking care of this feature request. It has not gone unnoticed.

Quick question: Are you planning to add more commits to this PR? Code changes? User manual? I am just asking, not demanding anything. I simply do not want to start testing too early. As for the user manual, I can of course take a look at the unit tests and take it from there. But that might not be true for all future extension developers, I am just speaking for myself.

@leonard84
Copy link
Member Author

@kriegaex I'll write some documentation for it, but I mainly wanted to get feedback on the feature first, like the one from @szpak concerning the usability.

try {
org.spockframework.runtime.SpockRuntime.callEnterBlock(this.getSpecificationContext(), new org.spockframework.runtime.model.BlockInfo(org.spockframework.runtime.model.BlockKind.WHEN, []))
foobar = this.foobar()
org.spockframework.runtime.SpockRuntime.callExitBlock(this.getSpecificationContext(), new org.spockframework.runtime.model.BlockInfo(org.spockframework.runtime.model.BlockKind.WHEN, []))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 this won't be called, fixing this makes for some awkward interactions between the individual AST transformations,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried moving the blockListener logic where the blocks are written back in SpecRewriter#L388, however this didn't work for some cases, e.g. cleanup as it already replaced all Blocks with an anonymous block.

@leonard84 leonard84 force-pushed the add-block-listener-support branch from c3f30eb to 0f3197e Compare May 7, 2023 05:41
@szpak
Copy link
Member

szpak commented Oct 2, 2023

As this PR seems a little bit stalled, I pushed the code I initially created ~6 months ago as a PoC for the BlockListener support. It a very raw version (with a lot of diagnostic stuff to be amended in the future), but functional in some basic scenarios. The JPA session, where requested, is flushed after the when block.

https://github.com/szpak/spock-jpa-flush-enforcer/tree/preview1

I would like to awake discussion about future shape of this PR.

@leonard84 @kriegaex WDYT?

@leonard84
Copy link
Member Author

It mostly hangs on the problems that adding the exit listener introduces #1575 (comment)

@leonard84
Copy link
Member Author

I think I've fixed the issues, but I need another fresh set of eyes to verify that everything looks good, I've stared at too many snapshots already.

@leonard84
Copy link
Member Author

@AndreasTu, as I mentioned earlier, I'll write some documentation and polish it later. Thanks for your comments in any case.

However, I'm looking for a review of the correctness of the implementation and generated code and general usability.

Copy link
Member

@szpak szpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've switched my experimental (and heavily work-in-progress) extension to the latest version and it still works as expected without any modification. Nice.

I will think about the possible corner cases, I might encounter there.

Btw, I wonder, if it is still a recommended way to decide if the block listener was intended for the current iteration?

Copy link
Member

@Vampire Vampire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some things missing in multiple places:

  • license headers
  • JavaDoc
  • documentation
  • @Beta
  • @since

Copy link
Member

Vampire commented Mar 21, 2024

Btw. could we also support where (and upcoming filter) blocks in a sensible way?

@leonard84
Copy link
Member Author

Btw. could we also support where (and upcoming filter) blocks in a sensible way?

I don't think it would be intuitive or really helpful.
They run outside the normal iteration.
When would you enter or leave the block?

  • only once when creating the data provider
  • on every data iterator's iteration?

However, you are most familiar with that part of the code.

@Vampire
Copy link
Member

Vampire commented Mar 22, 2024

When would you enter or leave the block?

No idea. :-D
I guess multiple times.
Maybe with an additional phase enum.
One for creating the data iterators.
One for getting the next set of data variable values.

But we can also start without and see where and how need arises.
But then it needs time to land in production version with our release cadence. :-D

@leonard84
Copy link
Member Author

Forgot to publish my responses 😅

@leonard84
Copy link
Member Author

Btw, I wonder, if it is still a recommended way to decide if the block listener was intended for the current iteration?

@szpak, can you motivate this use case more? At first glance, I'd try to register a block listener that can handle all iterations instead of one per iteration.

@szpak
Copy link
Member

szpak commented Apr 25, 2024

@szpak, can you motivate this use case more? At first glance, I'd try to register a block listener that can handle all iterations instead of one per iteration.

Hmm, I think the main problem was to get the invocation instance to obtain the current field instance. As a result, I put IBlockListener inside AbstractMethodInterceptor which provides invocation. Would you propose @leonard84 to pass the invocation instance to the listener in any simpler way?

@leonard84
Copy link
Member Author

@szpak, can you motivate this use case more? At first glance, I'd try to register a block listener that can handle all iterations instead of one per iteration.

Hmm, I think the main problem was to get the invocation instance to obtain the current field instance. As a result, I put IBlockListener inside AbstractMethodInterceptor which provides invocation. Would you propose @leonard84 to pass the invocation instance to the listener in any simpler way?

We could easily pass in the current instance. The question is whether we should.

I've also been debating whether I should give access to the current ISpockExecution so that a listener can get an IStore.

@szpak
Copy link
Member

szpak commented May 18, 2024

Hmm, I think the main problem was to get the invocation instance to obtain the current field instance. As a result, I put IBlockListener inside AbstractMethodInterceptor which provides invocation. Would you propose @leonard84 to pass the invocation instance to the listener in any simpler way?

We could easily pass in the current instance. The question is whether we should.

Definitely that's the good question. What are the design difference between interceptors (which have access to invocation) and listeners (also in work there are intended for)? The first could abort the processing, while listeners should not. Listeners should only perform (external) side effects? What also is different and if invocation in the second case could "complicate" something?

I've also been debating whether I should give access to the current ISpockExecution so that a listener can get an IStore.

Do you see any good cases where it would be necessary for listeners in practice? To read the values set by the other extensions/interceptors?

AndreasTu
AndreasTu previously approved these changes Oct 18, 2024
@leonard84 leonard84 enabled auto-merge (squash) October 19, 2024 16:54
Copy link
Member

@szpak szpak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a friendly reminder about the documentation (preferably with a small example of usage), before merging it.

@@ -27,6 +29,15 @@ public class BlockInfo {
private BlockKind kind;
private List<String> texts;

public BlockInfo() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could mark it as deprecated and possibly remove it in Spock 3.0 (or so)? Spock itself no longer calls the old constructor.
The setters could be also marked as deprecated.

szpak
szpak previously approved these changes Oct 19, 2024
Copy link
Member

@Vampire Vampire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments. :-)

Also, docs are of course still missing.

@@ -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_GetBlockCurrentBlock =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public final MethodNode SpecificationContext_GetBlockCurrentBlock =
public final MethodNode SpecificationContext_GetCurrentBlock =

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

public final MethodNode SpecificationContext_SetBlockCurrentBlock =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public final MethodNode SpecificationContext_SetBlockCurrentBlock =
public final MethodNode SpecificationContext_SetCurrentBlock =


public static BinaryExpression createVariableIsNotNullExpression(VariableExpression var) {
return new BinaryExpression(
new VariableExpression(var),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to create a new VariableExpression from the supplied one?


public static BinaryExpression createVariableIsNullExpression(VariableExpression var) {
return new BinaryExpression(
new VariableExpression(var),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to create a new VariableExpression from the supplied one?

new ConstantExpression(null));
}

public static BinaryExpression createVariableIsNullExpression(VariableExpression var) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intended that this unused method is added for some future use or is it a left-over from being used at some point?

@@ -152,7 +133,10 @@ public java.lang.Object $spock_feature_0_0proc(java.lang.Object $spock_p0) {
}

@Issue("https://github.com/spockframework/spock/issues/1287")
def "data variable with asserting closure produces error rethrower variable in data processor method"() {
def "data variable with asserting closure produces error rethrower variable in data processor method"(@Snapshot SpockSnapshotter snapshotter) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def "data variable with asserting closure produces error rethrower variable in data processor method"(@Snapshot SpockSnapshotter snapshotter) {
def "data variable with asserting closure produces error rethrower variable in data processor method"(@Snapshot(extension = 'groovy') SpockSnapshotter snapshotter) {

@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// set the block metaData index for each block this must be equal to the index of the block in the @BlockMetadata annotation
// set the block metadata index for each block this must be equal to the index of the block in the @BlockMetadata annotation

@@ -31,6 +31,7 @@ public abstract class Block extends Node<Method, List<Statement>> {
private final List<String> descriptions = new ArrayList<>(3);
private Block prev;
private Block next;
private int blockMetaDataIndex = -1;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private int blockMetaDataIndex = -1;
private int blockMetadataIndex = -1;

@@ -80,5 +81,25 @@ public boolean isFirstInChain() {
return isFirst() || getClass() != prev.getClass();
}

public void setBlockMetaDataIndex(int blockMetaDataIndex) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public void setBlockMetaDataIndex(int blockMetaDataIndex) {
public void setBlockMetadataIndex(int blockMetadataIndex) {

this.blockMetaDataIndex = blockMetaDataIndex;
}

public int getBlockMetaDataIndex() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public int getBlockMetaDataIndex() {
public int getBlockMetadataIndex() {

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.
Prior to this commit, IRunListener.error(ErrorInfo) didn't give any
context where the error happened.
@leonard84 leonard84 dismissed stale reviews from szpak and AndreasTu via bd9e694 November 1, 2024 14:01

private void addBlockListeners(Block block) {
BlockParseInfo blockType = block.getParseInfo();
if (blockType == BlockParseInfo.WHERE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is FILTER missing here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow access to the currently executing block object
6 participants