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

Support defining conditions in @Verify/@VerifyAll helper methods #2112

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

Conversation

pshevche
Copy link
Contributor

Description

The PR adds two new annotations: @spock.lang.Verify and @spock.lang.VerifyAll that can be applied to helper methods containing condition blocks. Top-level implicit or explicit assertion statements in these methods will be rewritten using the ConditionRewriter. @Verify and @VerifyAll helpers can be defined in any class, not only in the ones extending from Specification. The method is required to have a void return type.

Reuse the `ConditionRewriter` to transform condition expressions in methods annotated with `spock.lang.Verify`. The method can be defined in any class, not only the ones extending from `Specification`. The method is required to have a `void` return type. Implicit condition assertions on all levels will be transformed

Signed-off-by: Pavlo Shevchenko <[email protected]>
Reuse the `VerifyMethodsRewriter` to rewrite implicit conditions in helper methods annotated with `@VerifyAll`. The only difference is to use `ErrorCollector` instead of `ErrorRethrower` to not rethrow the exception immediately, but to collect all failures and throw them at the end.

Signed-off-by: Pavlo Shevchenko <[email protected]>
Signed-off-by: Pavlo Shevchenko <[email protected]>
Signed-off-by: Pavlo Shevchenko <[email protected]>
Signed-off-by: Pavlo Shevchenko <[email protected]>
@pshevche pshevche force-pushed the pshevche/implicit-assertions-in-helper-methods branch from cf60234 to 1ccb09b Compare February 24, 2025 19:38
@pshevche pshevche force-pushed the pshevche/implicit-assertions-in-helper-methods branch from 1ccb09b to ebe0044 Compare February 24, 2025 19:41
Signed-off-by: Pavlo Shevchenko <[email protected]>
@@ -276,7 +277,7 @@ public Store(IterationInfo iterationInfo, Path rootPath, boolean updateSnapshots
this.extension = Assert.notNull(extension);
this.charset = Assert.notNull(charset);

Class<?> specClass = iterationInfo.getFeature().getSpec().getReflection();
Class<?> specClass = iterationInfo.getFeature().getParent().getBottomSpec().getReflection();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

💬 Allows defining snapshot-based in base classes, but child specs can overwrite snapshots.

Copy link
Member

Choose a reason for hiding this comment

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

This is a potentially breaking change, but it hasn't been in a final release yet. So it should be fine. We should document it in the release notes nonetheless.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added release notes in 49d2685 (#2112). I think there is value in supporting both scenarios: you may want to define features and snapshots for base class, but overwrite them for certain features.

Copy link

codecov bot commented Feb 24, 2025

Codecov Report

Attention: Patch coverage is 93.49593% with 8 lines in your changes missing coverage. Please review.

Project coverage is 82.03%. Comparing base (bc82305) to head (49d2685).
Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
.../compiler/condition/BaseVerifyMethodTransform.java 85.00% 3 Missing ⚠️
.../java/org/spockframework/runtime/SpockRuntime.java 50.00% 2 Missing ⚠️
...k/compiler/condition/BaseVerifyMethodRewriter.java 96.66% 0 Missing and 1 partial ⚠️
...rk/compiler/condition/ImplicitConditionsUtils.java 85.71% 0 Missing and 1 partial ⚠️
...ock-core/src/main/java/spock/lang/Snapshotter.java 50.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##             master    #2112      +/-   ##
============================================
+ Coverage     81.90%   82.03%   +0.12%     
- Complexity     4692     4728      +36     
============================================
  Files           452      463      +11     
  Lines         14696    14771      +75     
  Branches       1852     1856       +4     
============================================
+ Hits          12037    12117      +80     
+ Misses         1975     1974       -1     
+ Partials        684      680       -4     
Files with missing lines Coverage Δ
...org/spockframework/compiler/ConditionRewriter.java 90.54% <100.00%> (+0.04%) ⬆️
...org/spockframework/compiler/DeepBlockRewriter.java 98.77% <100.00%> (+0.53%) ⬆️
...g/spockframework/compiler/InteractionRewriter.java 91.79% <100.00%> (ø)
...java/org/spockframework/compiler/SpecRewriter.java 93.76% <100.00%> (+0.06%) ⬆️
...rg/spockframework/compiler/WhereBlockRewriter.java 94.74% <100.00%> (ø)
...iler/condition/DefaultConditionErrorRecorders.java 100.00% <100.00%> (ø)
...r/condition/DefaultConditionRewriterResources.java 100.00% <100.00%> (ø)
...k/compiler/condition/IConditionErrorRecorders.java 100.00% <100.00%> (ø)
...rk/compiler/condition/VerifyAllMethodRewriter.java 100.00% <100.00%> (ø)
...k/compiler/condition/VerifyAllMethodTransform.java 100.00% <100.00%> (ø)
... and 9 more

... and 7 files with indirect coverage changes

@@ -276,7 +277,7 @@ public Store(IterationInfo iterationInfo, Path rootPath, boolean updateSnapshots
this.extension = Assert.notNull(extension);
this.charset = Assert.notNull(charset);

Class<?> specClass = iterationInfo.getFeature().getSpec().getReflection();
Class<?> specClass = iterationInfo.getFeature().getParent().getBottomSpec().getReflection();
Copy link
Member

Choose a reason for hiding this comment

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

This is a potentially breaking change, but it hasn't been in a final release yet. So it should be fine. We should document it in the release notes nonetheless.

@leonard84 leonard84 added this to the 2.4-M6 milestone Feb 25, 2025
@leonard84 leonard84 self-assigned this Feb 25, 2025
@pshevche pshevche requested a review from leonard84 February 28, 2025 09:50
snapshotter.assertThat(result.source).matchesSnapshot()
}

def "transforms only top-level conditions"() {
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we want to keep that behavior for these annotations. It is a classic foot gun. If it wouldn't break anything I would probably do away with that restriction in the Spec itself. However, that would have to wait until Spock 3.

Inside with or verifyAll we already support assertions in if and so on.

Thoughts: @Vampire, @AndreasTu ?

Copy link
Member

Choose a reason for hiding this comment

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

I would not keep this behavior for the new feature, if it is simple to change it for the new one and preserve the old behavior in specs until Spock 3.

processVerificationHelperMethod((MethodNode) node, errorReporter, sourceLookup);
}
} finally {
sourceLookup.close();
Copy link
Member

Choose a reason for hiding this comment

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

🤔 should we make SourceLookup AutoClosable so we could use it in try-with?


then:
result.failures.size() == 1
with(result.failures[0].exception, MultipleFailuresError) {
Copy link
Member

Choose a reason for hiding this comment

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

Isn't it

Suggested change
with(result.failures[0].exception, MultipleFailuresError) {
with(result.failures[0].exception, SpockMultipleFailuresError) {

Copy link
Member

@AndreasTu AndreasTu left a comment

Choose a reason for hiding this comment

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

Thanks a lot for your work! 👍
I like the idea for these annotations.

* may contain conditions, allowing to omit the {@code assert} keyword.
* As in expect-blocks and then-blocks, variable declarations
* and void method invocations will not be considered conditions.
* The method can be defined in the {@link Specification} class or in a separate class.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add that it needs to be a Groovy class.

Suggested change
* The method can be defined in the {@link Specification} class or in a separate class.
* The method can be defined in the {@link Specification} class or in a separate Groovy class.

* and void method invocations will not be considered conditions.
* The method can be defined in the {@link Specification} class or in a separate class.
* The method must have a {@code void} return type.
* The test will fail on the first failing assertion.
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
* The test will fail on the first failing assertion.
* A test calling the method will fail on the first failing assertion.

* may contain conditions, allowing to omit the {@code assert} keyword.
* As in expect-blocks and then-blocks, variable declarations
* and void method invocations will not be considered conditions.
* The method can be defined inside the {@link Specification} class or in a separate class.
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
* The method can be defined inside the {@link Specification} class or in a separate class.
* The method can be defined inside the {@link Specification} class or in a separate Groovy class.

snapshotter.assertThat(result.source).matchesSnapshot()
}

def "transforms only top-level conditions"() {
Copy link
Member

Choose a reason for hiding this comment

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

I would not keep this behavior for the new feature, if it is simple to change it for the new one and preserve the old behavior in specs until Spock 3.

@@ -23,7 +21,7 @@ public void validateCollectedErrors() throws Throwable {
throw throwables.get(0);

default:
throw new MultipleFailuresError("", throwables);
throw new SpockMultipleFailuresError("", throwables);
Copy link
Member

Choose a reason for hiding this comment

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

Maybe document in the release_notes that now the subclass SpockMultipleFailuresError is thrown instead of MultipleFailuresError.
Sometimes people do strange checks, and this would at least docoment the change, although it is a compatible one.

Comment on lines +35 to +38
this.nodeCache = nodeCache;
this.lookup = lookup;
this.errorReporter = errorReporter;
this.errorRecorders = errorRecorders;
Copy link
Member

Choose a reason for hiding this comment

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

Please add Objects.requireNonNull() to indicate non-null params or add @Nulable annotations.

Suggested change
this.nodeCache = nodeCache;
this.lookup = lookup;
this.errorReporter = errorReporter;
this.errorRecorders = errorRecorders;
this.nodeCache = requireNonNull(nodeCache);
this.lookup = requireNonNull(lookup);
this.errorReporter = requireNonNull(errorReporter);
this.errorRecorders = requireNonNull(errorRecorders);

Copy link
Member

Choose a reason for hiding this comment

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

For internal code it should be org.spockframework.util.Assert#notNull(T) and for user facing code it would be org.spockframework.util.Checks#notNull.

private final AstNodeCache nodeCache;

public DefaultConditionErrorRecorders(AstNodeCache nodeCache) {
this.nodeCache = nodeCache;
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
this.nodeCache = nodeCache;
this.nodeCache = requireNonNull(nodeCache);

Comment on lines +40 to +41
this.resources = resources;
this.methodNode = methodNode;
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
this.resources = resources;
this.methodNode = methodNode;
this.resources = requireNonNull(resources);
this.methodNode = requireNonNull(methodNode);

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.

3 participants