Skip to content

Commit cf77438

Browse files
pshevcheleonard84
andauthored
Support defining conditions in @Verify/@VerifyAll helper methods (#2112)
## 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. --------- Signed-off-by: Pavlo Shevchenko <pavel.shevchenko.95@gmail.com> Co-authored-by: Leonard Brünings <lbruenings@gradle.com>
1 parent 6ab9c20 commit cf77438

File tree

54 files changed

+1719
-141
lines changed

Some content is hidden

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

54 files changed

+1719
-141
lines changed

docs/release_notes.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,23 @@ include::include.adoc[]
55

66
== 2.4-M6 (tbd)
77

8+
=== Highlights
9+
10+
* Add support for defining condition blocks with implicit assertions in helper methods annotated with `@Verify` or `@VerifyAll` spockPull:2112[]
11+
12+
=== Breaking Changes
13+
14+
* _This affects users of the `@Snapshot` extension, only if you were using the snapshotter in parent specification classes._ +
15+
`@Snapshot` used to look up snapshots in directories named after the class containing feature methods. Now, the snapshots will be loaded from directories named after the bottom class in the specification hierarchy. The motivation of the change is to allow users to define features in base specification classes, but overwrite expected snapshots per child specification.
16+
spockPull:2112[]
17+
818
=== Misc
919

1020
* Fix ExtensionException in OSGi environment for global extension spockIssue:2076[]
1121
** This issue was introduced with spockPull:1995[]
1222
* Fix `@RestoreSystemProperties` not restoring state between iterations of a data-driven feature spockIssue:2104[]
1323
* Fix `VerifyError: Stack map does not match the one at exception handler` introduced in 2.4-M5 spockIssue:2080[]
24+
* Throw `SpockMultipleFailuresError` instead of a generic `MultipleFailuresError` in case of multiple failed assertions spockPull:2112[]
1425

1526
== 2.4-M5 (2025-01-07)
1627

docs/spock_primer.adoc

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -576,30 +576,19 @@ false ...
576576

577577
Not very helpful. Fortunately, we can do better:
578578

579-
[source,groovy]
579+
[source,groovy,indent=0]
580580
----
581-
void matchesPreferredConfiguration(pc) {
582-
assert pc.vendor == "Sunny"
583-
assert pc.clockRate >= 2333
584-
assert pc.ram >= 4096
585-
assert pc.os == "Linux"
586-
}
581+
include::{sourcedir}/primer/VerifyMethodsDocSpec.groovy[tag=verify-helper-method]
587582
----
588583

589-
When factoring out conditions into a helper method, two points need to be considered: First, implicit conditions must
590-
be turned into explicit conditions with the `assert` keyword. Second, the helper method must have return type `void`.
584+
When factoring out conditions into a helper method, you need to ensure the method has return type `void`.
591585
Otherwise, Spock might interpret the return value as a failing condition, which is not what we want.
592586

593587
As expected, the improved helper method tells us exactly what's wrong:
594588

595-
[source,groovy]
589+
[source,groovy,indent=0]
596590
----
597-
Condition not satisfied:
598-
599-
assert pc.clockRate >= 2333
600-
| | |
601-
| 1666 false
602-
...
591+
include::{sourcedir}/primer/VerifyMethodsDocSpec.groovy[tag=verify-helper-method-result]
603592
----
604593

605594
A final advice: Although code reuse is generally a good thing, don't take it too far. Be aware that the use of fixture
@@ -651,7 +640,7 @@ with(service) {
651640
Sometimes an IDE has trouble to determine the type of the target, in that case you can help out by manually specifying the
652641
target type via `with(target, type, closure)`.
653642

654-
== Using `verifyAll` to assert multiple expectations together
643+
== Using `verifyAll` or `@VerifyAll` helper methods to assert multiple expectations together
655644

656645
Normal expectations fail the test on the first failed assertions. Sometimes it is helpful to collect these failures before
657646
failing the test to have more information, this behavior is also known as soft assertions.
@@ -676,7 +665,6 @@ def "offered PC matches preferred configuration"() {
676665

677666
or it can be used without a target.
678667

679-
680668
[source,groovy]
681669
----
682670
expect:
@@ -688,6 +676,13 @@ or it can be used without a target.
688676

689677
Like `with` you can also optionally define a type hint for the IDE.
690678

679+
Alternatively, `verifyAll` conditions can be extracted into a helper method annotated with `@VerifyAll`
680+
681+
[source,groovy,indent=0]
682+
----
683+
include::{sourcedir}/primer/VerifyMethodsDocSpec.groovy[tag=verify-all-helper-method]
684+
----
685+
691686
== Using `verifyEach` to assert on each element of an `Iterable`
692687

693688
There are several ways to do assertions on `Iterable` or `Collection`.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,21 @@ private ConditionRewriter(IRewriteResources resources, String valueRecorderSuffi
8080
errorCollectorName = SpockNames.ERROR_COLLECTOR + errorCollectorSuffix;
8181
}
8282

83+
public static Statement rewriteExplicitCondition(AssertStatement stat, IRewriteResources resources) {
84+
return rewriteExplicitCondition(stat, resources, "", "");
85+
}
86+
8387
public static Statement rewriteExplicitCondition(AssertStatement stat, IRewriteResources resources,
8488
String valueRecorderSuffix, String errorCollectorSuffix) {
8589
ConditionRewriter rewriter = new ConditionRewriter(resources, valueRecorderSuffix, errorCollectorSuffix);
8690
Expression message = AstUtil.getAssertionMessage(stat);
8791
return rewriter.rewriteCondition(stat, stat.getBooleanExpression().getExpression(), message, true);
8892
}
8993

94+
public static Statement rewriteImplicitCondition(ExpressionStatement stat, IRewriteResources resources) {
95+
return rewriteImplicitCondition(stat, resources, "", "");
96+
}
97+
9098
public static Statement rewriteImplicitCondition(ExpressionStatement stat, IRewriteResources resources,
9199
String valueRecorderSuffix, String errorCollectorSuffix) {
92100
ConditionRewriter rewriter = new ConditionRewriter(resources, valueRecorderSuffix, errorCollectorSuffix);

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

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import java.util.List;
2929

3030
import static org.codehaus.groovy.ast.expr.MethodCallExpression.NO_ARGUMENTS;
31+
import static org.spockframework.compiler.condition.ImplicitConditionsUtils.checkIsValidImplicitCondition;
32+
import static org.spockframework.compiler.condition.ImplicitConditionsUtils.isImplicitCondition;
3133

3234
/**
3335
* Walks the statement and expression tree to:
@@ -41,12 +43,12 @@
4143
* @author Peter Niederwieser
4244
*/
4345
public class DeepBlockRewriter extends AbstractDeepBlockRewriter {
44-
private final IRewriteResources resources;
46+
private final ISpecRewriteResources resources;
4547
private boolean insideInteraction = false;
4648
private int interactionClosureDepth = 0;
4749
private int closureDepth = 0;
4850

49-
public DeepBlockRewriter(IRewriteResources resources) {
51+
public DeepBlockRewriter(ISpecRewriteResources resources) {
5052
super(resources.getCurrentBlock(), resources.getAstNodeCache());
5153
this.resources = resources;
5254
}
@@ -200,7 +202,7 @@ private boolean handleImplicitCondition(ExpressionStatement stat) {
200202
}
201203
if (!isImplicitCondition(stat)) return false;
202204

203-
checkIsValidImplicitCondition(stat);
205+
checkIsValidImplicitCondition(stat, resources.getErrorReporter());
204206

205207
String methodName = AstUtil.getMethodName(stat.getExpression());
206208
boolean isConditionMethodCall = Identifiers.CONDITION_METHODS.contains(methodName);
@@ -304,10 +306,10 @@ private boolean handleInteractionBlockCall(MethodCallExpression expr) {
304306

305307
private void defineRecorders(ClosureExpression expr) {
306308
if (groupConditionFound) {
307-
resources.defineErrorCollector(AstUtil.getStatements(expr), getErrorCollectorSuffix());
309+
resources.getErrorRecorders().defineErrorCollector(AstUtil.getStatements(expr), getErrorCollectorSuffix());
308310
}
309311
if (conditionFound) {
310-
resources.defineValueRecorder(AstUtil.getStatements(expr), getValueRecorderSuffix());
312+
resources.getErrorRecorders().defineValueRecorder(AstUtil.getStatements(expr), getValueRecorderSuffix());
311313
}
312314
}
313315

@@ -350,19 +352,4 @@ private boolean isInteractionExpression(InteractionRewriter rewriter, Expression
350352
return false;
351353
}
352354
}
353-
354-
// assumption: not already an interaction
355-
public static boolean isImplicitCondition(Statement stat) {
356-
return stat instanceof ExpressionStatement
357-
&& !(((ExpressionStatement) stat).getExpression() instanceof DeclarationExpression);
358-
}
359-
360-
private void checkIsValidImplicitCondition(Statement stat) {
361-
BinaryExpression binExpr = AstUtil.getExpression(stat, BinaryExpression.class);
362-
if (binExpr == null) return;
363-
364-
if (Types.ofType(binExpr.getOperation().getType(), Types.ASSIGNMENT_OPERATOR)) {
365-
resources.getErrorReporter().error(stat, "Expected a condition, but found an assignment. Did you intend to write '==' ?");
366-
}
367-
}
368355
}

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

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,21 @@
1616

1717
package org.spockframework.compiler;
1818

19-
import java.util.List;
20-
2119
import org.codehaus.groovy.ast.ASTNode;
22-
import org.codehaus.groovy.ast.expr.Expression;
23-
import org.codehaus.groovy.ast.expr.MethodCallExpression;
24-
import org.codehaus.groovy.ast.expr.VariableExpression;
25-
import org.codehaus.groovy.ast.stmt.Statement;
20+
import org.spockframework.compiler.condition.IConditionErrorRecorders;
2621

27-
import org.spockframework.compiler.model.Block;
28-
import org.spockframework.compiler.model.Method;
29-
import org.spockframework.compiler.model.Spec;
22+
import java.util.List;
3023

3124
/**
32-
*
3325
* @author Peter Niederwieser
3426
*/
3527
public interface IRewriteResources {
36-
Spec getCurrentSpec();
37-
Method getCurrentMethod();
38-
Block getCurrentBlock();
39-
40-
void defineValueRecorder(List<Statement> stats, String variableNameSuffix);
41-
void defineErrorRethrower(List<Statement> stats);
42-
void defineErrorCollector(List<Statement> stats, String variableNameSuffix);
43-
VariableExpression captureOldValue(Expression oldValue);
44-
MethodCallExpression getMockInvocationMatcher();
45-
4628
AstNodeCache getAstNodeCache();
29+
4730
String getSourceText(ASTNode node);
31+
4832
ErrorReporter getErrorReporter();
33+
34+
IConditionErrorRecorders getErrorRecorders();
35+
4936
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.spockframework.compiler;
18+
19+
import org.codehaus.groovy.ast.expr.Expression;
20+
import org.codehaus.groovy.ast.expr.MethodCallExpression;
21+
import org.codehaus.groovy.ast.expr.VariableExpression;
22+
import org.spockframework.compiler.model.Block;
23+
import org.spockframework.compiler.model.Method;
24+
import org.spockframework.compiler.model.Spec;
25+
26+
public interface ISpecRewriteResources extends IRewriteResources {
27+
Spec getCurrentSpec();
28+
29+
Method getCurrentMethod();
30+
31+
Block getCurrentBlock();
32+
33+
VariableExpression captureOldValue(Expression oldValue);
34+
35+
MethodCallExpression getMockInvocationMatcher();
36+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
* @author Peter Niederwieser
3737
*/
3838
public class InteractionRewriter {
39-
private final IRewriteResources resources;
39+
private final ISpecRewriteResources resources;
4040
private final ClosureExpression activeWithOrMockClosure;
4141

4242
// information about the interaction; filled in by parse()
@@ -52,7 +52,7 @@ public class InteractionRewriter {
5252
// "new InteractionBuilder(..).setCount(..).setTarget(..).setMethod(..).addArg(..).addResult(..).build()"
5353
private Expression builderExpr;
5454

55-
public InteractionRewriter(IRewriteResources resources, @Nullable ClosureExpression activeWithOrMockClosure) {
55+
public InteractionRewriter(ISpecRewriteResources resources, @Nullable ClosureExpression activeWithOrMockClosure) {
5656
this.resources = resources;
5757
this.activeWithOrMockClosure = activeWithOrMockClosure;
5858
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import org.codehaus.groovy.control.Janitor;
2121
import org.codehaus.groovy.control.SourceUnit;
2222

23-
public class SourceLookup {
23+
public class SourceLookup implements AutoCloseable {
2424
private final SourceUnit sourceUnit;
2525
private final Janitor janitor = new Janitor();
2626

@@ -48,6 +48,7 @@ public String lookup(ASTNode node) {
4848
return text.toString().trim();
4949
}
5050

51+
@Override
5152
public void close() {
5253
janitor.cleanup();
5354
}

0 commit comments

Comments
 (0)