Skip to content

Commit 1f3b694

Browse files
authored
Add filter block to filter iterations (#1927)
Follow-up after #1298
1 parent 536d326 commit 1f3b694

28 files changed

+600
-28
lines changed

docs/data_driven_testing.adoc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,23 @@ yield different numbers of iterations. If a data provider runs out of values soo
442442
Variable assignments don't affect the number of iterations. A `where:` block that only contains assignments yields
443443
exactly one iteration.
444444

445+
== Filtering iterations
446+
447+
If you want to filter out some iterations, you can use the `@IgnoreIf` annotation on the feature method.
448+
This has one significant drawback though, the iteration would be reported as skipped in test reports.
449+
Therefor you can have a `filter` block after the `where` block.
450+
The content of this block is treated like the content of the `exepct` block.
451+
If any of the implicit or explicit assertions in the `filter` block fails, the iteration is treated like it would not exist.
452+
This also means, that if all iterations are filtered out, the test will fail like when giving a data provider without content.
453+
454+
In the following example the test is executed with the values `1`, `2`, `4`, and `5` for the variable `i`,
455+
the iteration where `i` would be `3` is filtered out by the `filter` block:
456+
457+
[source,groovy,indent=0]
458+
----
459+
include::{sourcedir}/datadriven/DataSpec.groovy[tag=excluding-iterations]
460+
----
461+
445462
== Closing of Data Providers
446463

447464
After all iterations have completed, the zero-argument `close` method is called on all data providers that have

docs/release_notes.adoc

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

1010
* Add support for combining two or more data providers using cartesian product spockIssue:1062[]
11+
* Add support for a `filter` block after a `where` block to filter out unwanted iterations
1112

1213
== 2.4-M4 (2024-03-21)
1314

docs/spock_primer.adoc

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ interacting feature methods), and may occur more than once.
148148

149149
Spock has built-in support for implementing each of the conceptual phases of a feature method. To this end, feature
150150
methods are structured into so-called _blocks_. Blocks start with a label, and extend to the beginning of the next block,
151-
or the end of the method. There are six kinds of blocks: `given`, `when`, `then`, `expect`, `cleanup`, and `where` blocks.
151+
or the end of the method. There are seven kinds of blocks: `given`, `when`, `then`, `expect`, `cleanup`, `where`, and `filter` blocks.
152152
Any statements between the beginning of the method and the first explicit block belong to an implicit `given` block.
153153

154154
A feature method must have at least one explicit (i.e. labelled) block - in fact, the presence of an explicit block is
@@ -483,7 +483,7 @@ TIP: If a specification is designed in such a way that all its feature methods r
483483

484484
==== Where Blocks
485485

486-
A `where` block always comes last in a method, and may not be repeated. It is used to write data-driven feature methods.
486+
A `where` block may only be followed by a `filter` block, and may not be repeated. It is used to write data-driven feature methods.
487487
To give you an idea how this is done, have a look at the following example:
488488

489489
[source,groovy]
@@ -506,6 +506,21 @@ Although it is declared last, the `where` block is evaluated before the feature
506506

507507
The `where` block is further explained in the <<data_driven_testing.adoc#data-driven-testing,Data Driven Testing>> chapter.
508508

509+
==== Filter Blocks
510+
511+
A `filter` block always comes last in a method, and may not be repeated. It is used to filter iterations in data-driven feature methods.
512+
To give you an idea how this is done, have a look at the following example:
513+
514+
[source,groovy,indent=0]
515+
----
516+
include::{sourcedir}/datadriven/DataSpec.groovy[tag=excluding-iterations]
517+
----
518+
519+
The content of the `filter` block is treated like the content of an `expect` block. If any of the implicit or explicit
520+
assertions in it fail for a given iteration, this iteration is skipped.
521+
522+
The `filter` block is further explained in the <<data_driven_testing.adoc#data-driven-testing,Data Driven Testing>> chapter.
523+
509524
== Helper Methods
510525

511526
Sometimes feature methods grow large and/or contain lots of duplicated code. In such cases it can make sense to introduce

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ public void visitThenBlock(ThenBlock block) throws Exception {}
4646
public void visitCleanupBlock(CleanupBlock block) throws Exception {}
4747
@Override
4848
public void visitWhereBlock(WhereBlock block) throws Exception {}
49+
@Override
50+
public void visitFilterBlock(FilterBlock block) throws Exception {}
4951
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ private boolean handleInteraction(InteractionRewriter rewriter, ExpressionStatem
191191
}
192192

193193
private boolean handleImplicitCondition(ExpressionStatement stat) {
194-
if (!(stat == currTopLevelStat && isThenOrExpectBlock()
194+
if (!(stat == currTopLevelStat && isThenOrExpectOrFilterBlock()
195195
|| currSpecialMethodCall.isConditionMethodCall()
196196
|| currSpecialMethodCall.isConditionBlock()
197197
|| currSpecialMethodCall.isGroupConditionBlock()
@@ -339,8 +339,8 @@ private ClosureExpression getCurrentWithOrMockClosure() {
339339
return null;
340340
}
341341

342-
private boolean isThenOrExpectBlock() {
343-
return (block instanceof ThenBlock || block instanceof ExpectBlock);
342+
private boolean isThenOrExpectOrFilterBlock() {
343+
return (block instanceof ThenBlock || block instanceof ExpectBlock || block instanceof FilterBlock);
344344
}
345345

346346
private boolean isInteractionExpression(InteractionRewriter rewriter, ExpressionStatement stat) {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,4 +237,8 @@ public void visitCleanupBlock(CleanupBlock block) throws Exception {
237237
public void visitWhereBlock(WhereBlock block) throws Exception {
238238
addBlockMetadata(block, BlockKind.WHERE);
239239
}
240+
241+
public void visitFilterBlock(FilterBlock block) throws Exception {
242+
addBlockMetadata(block, BlockKind.FILTER);
243+
}
240244
}

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,22 @@ private ClassNode getPlainReference(ClassNode type) {
368368
// s.t. missing method parameters are added; these parameters
369369
// will then be used by DeepBlockRewriter
370370
private void handleWhereBlock(Method method) {
371-
Block block = method.getLastBlock();
372-
if (!(block instanceof WhereBlock)) return;
371+
Block lastblock = method.getLastBlock();
372+
FilterBlock filterBlock;
373+
WhereBlock whereBlock;
374+
if (lastblock instanceof FilterBlock) {
375+
filterBlock = (FilterBlock) lastblock;
376+
whereBlock = (WhereBlock) lastblock.getPrevious();
377+
} else if (lastblock instanceof WhereBlock) {
378+
filterBlock = null;
379+
whereBlock = (WhereBlock) lastblock;
380+
} else {
381+
return;
382+
}
373383

374384
DeepBlockRewriter deep = new DeepBlockRewriter(this);
375-
deep.visit(block);
376-
WhereBlockRewriter.rewrite((WhereBlock) block, this, deep.isDeepNonGroupedConditionFound());
385+
deep.visit(whereBlock);
386+
WhereBlockRewriter.rewrite(whereBlock, filterBlock, this, deep.isDeepNonGroupedConditionFound());
377387
}
378388

379389
@Override
@@ -501,7 +511,7 @@ public void visitCleanupBlock(CleanupBlock block) {
501511

502512
method.getStatements().add(tryFinally);
503513

504-
// a cleanup-block may only be followed by a where-block, whose
514+
// a cleanup-block may only be followed by a where-block and filter-block, whose
505515
// statements are copied to newly generated methods rather than
506516
// the original method
507517
movedStatsBackToMethod = true;

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

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.spockframework.compiler;
1818

19+
import org.spockframework.compiler.model.FilterBlock;
1920
import org.spockframework.compiler.model.WhereBlock;
2021
import org.spockframework.runtime.model.DataProcessorMetadata;
2122
import org.spockframework.runtime.model.DataProviderMetadata;
@@ -46,6 +47,7 @@
4647
*/
4748
public class WhereBlockRewriter {
4849
private final WhereBlock whereBlock;
50+
private final FilterBlock filterBlock;
4951
private final IRewriteResources resources;
5052
private final boolean defineErrorRethrower;
5153
private final InstanceFieldAccessChecker instanceFieldAccessChecker;
@@ -64,16 +66,17 @@ public class WhereBlockRewriter {
6466
private final List<Expression> dataVariableMultiplications = new ArrayList<>();
6567
private int localVariableCount = 0;
6668

67-
private WhereBlockRewriter(WhereBlock whereBlock, IRewriteResources resources, boolean defineErrorRethrower) {
69+
private WhereBlockRewriter(WhereBlock whereBlock, FilterBlock filterBlock, IRewriteResources resources, boolean defineErrorRethrower) {
6870
this.whereBlock = whereBlock;
71+
this.filterBlock = filterBlock;
6972
this.resources = resources;
7073
this.defineErrorRethrower = defineErrorRethrower;
7174
instanceFieldAccessChecker = new InstanceFieldAccessChecker(resources);
7275
errorRethrowerUsageDetector = defineErrorRethrower ? new ErrorRethrowerUsageDetector() : null;
7376
}
7477

75-
public static void rewrite(WhereBlock block, IRewriteResources resources, boolean defineErrorRethrower) {
76-
new WhereBlockRewriter(block, resources, defineErrorRethrower).rewrite();
78+
public static void rewrite(WhereBlock block, FilterBlock filterBlock, IRewriteResources resources, boolean defineErrorRethrower) {
79+
new WhereBlockRewriter(block, filterBlock, resources, defineErrorRethrower).rewrite();
7780
}
7881

7982
private void rewrite() {
@@ -144,6 +147,7 @@ private void rewrite() {
144147
handleFeatureParameters();
145148
createDataProcessorMethod();
146149
createDataVariableMultiplicationsMethod();
150+
createFilterMethod();
147151
}
148152

149153
private static boolean isMultiplicand(Statement stat) {
@@ -879,6 +883,40 @@ private void createDataVariableMultiplicationsMethod() {
879883
whereBlock.getParent().getParent().getAst().addMethod(dataVariableMultiplicationsMethod);
880884
}
881885

886+
private void createFilterMethod() {
887+
if (dataProcessorVars.isEmpty() || (filterBlock == null)) return;
888+
889+
DeepBlockRewriter deep = new DeepBlockRewriter(resources);
890+
deep.visit(filterBlock);
891+
892+
List<Statement> filterStats = new ArrayList<>(filterBlock.getAst());
893+
filterBlock.getAst().clear();
894+
895+
instanceFieldAccessChecker.check(filterStats);
896+
897+
if (deep.isConditionFound()) {
898+
resources.defineValueRecorder(filterStats, "");
899+
}
900+
if (deep.isDeepNonGroupedConditionFound()) {
901+
resources.defineErrorRethrower(filterStats);
902+
}
903+
904+
BlockStatement blockStat = new BlockStatement(filterStats, null);
905+
906+
MethodNode filterMethod = new MethodNode(
907+
InternalIdentifiers.getFilterName(filterBlock.getParent().getAst().getName()),
908+
Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC,
909+
ClassHelper.VOID_TYPE,
910+
dataProcessorVars
911+
.stream()
912+
.map(variable -> new Parameter(ClassHelper.OBJECT_TYPE, variable.getName()))
913+
.toArray(Parameter[]::new),
914+
ClassNode.EMPTY_ARRAY,
915+
blockStat);
916+
917+
filterBlock.getParent().getParent().getAst().addMethod(filterMethod);
918+
}
919+
882920
private static InvalidSpecCompileException notAParameterization(ASTNode stat) {
883921
return new InvalidSpecCompileException(stat,
884922
"where-blocks may only contain parameterizations (e.g. 'salary << [1000, 5000, 9000]; salaryk = salary / 1000')");

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public Block addNewBlock(Method method) {
118118
}
119119
@Override
120120
public EnumSet<BlockParseInfo> getSuccessors(Method method) {
121-
return EnumSet.of(AND, COMBINED, METHOD_END);
121+
return EnumSet.of(AND, COMBINED, FILTER, METHOD_END);
122122
}
123123
},
124124

@@ -133,6 +133,17 @@ public EnumSet<BlockParseInfo> getSuccessors(Method method) {
133133
}
134134
},
135135

136+
FILTER {
137+
@Override
138+
public Block addNewBlock(Method method) {
139+
return method.addBlock(new FilterBlock(method));
140+
}
141+
@Override
142+
public EnumSet<BlockParseInfo> getSuccessors(Method method) {
143+
return EnumSet.of(AND, METHOD_END);
144+
}
145+
},
146+
136147
METHOD_END {
137148
@Override
138149
public Block addNewBlock(Method method) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2024 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.model;
18+
19+
/**
20+
* AST node representing a filter-block in a feature method.
21+
*/
22+
public class FilterBlock extends Block {
23+
public FilterBlock(Method parent) {
24+
super(parent);
25+
setName("filter");
26+
}
27+
28+
@Override
29+
public void accept(ISpecVisitor visitor) throws Exception {
30+
visitor.visitAnyBlock(this);
31+
visitor.visitFilterBlock(this);
32+
}
33+
34+
@Override
35+
public BlockParseInfo getParseInfo() {
36+
return BlockParseInfo.FILTER;
37+
}
38+
}

0 commit comments

Comments
 (0)