Skip to content

Commit fe08360

Browse files
authored
Add !! as opt-out operator for assertions... (#1532)
1 parent 6312c3a commit fe08360

File tree

4 files changed

+236
-25
lines changed

4 files changed

+236
-25
lines changed

docs/spock_primer.adoc

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,62 @@ def setup() {
242242

243243
If an explicit condition is violated, it will produce the same nice diagnostic message as an implicit condition.
244244

245+
====== Opt-out of condition handling
246+
247+
Sometimes, you may want to use a top-level expression that is not treated as a condition.
248+
249+
For example, you may want to loop over a list with `each` and do assertions on its items.
250+
251+
[source,groovy,indent=0]
252+
----
253+
include::{sourcedir}/primer/ConditionsSpec.groovy[tag=each-assert-normal]
254+
----
255+
256+
However, this will fail if the list is empty, as `each` returns the list again and an empty list is falsy according to Groovy-truth.
257+
258+
To avoid this, you can use the `!!` operator to opt-out of condition handling:
259+
[source,groovy,indent=0]
260+
----
261+
include::{sourcedir}/primer/ConditionsSpec.groovy[tag=each-assert-opt-out-operator]
262+
----
263+
264+
The `!!` operator can also be used to hide the contents of an expression in an explicit assertion statement.
265+
This can be helpful, if the rendered expression is too long to be readable, or if you want to hide the data itself.
266+
267+
268+
[source,groovy,indent=0]
269+
----
270+
include::{sourcedir}/primer/ConditionsSpec.groovy[tag=explicit-with-opt-out-operator-and-message]
271+
----
272+
273+
will be rendered as
274+
275+
----
276+
include::{sourcedir}/primer/ConditionsSpec.groovy[tag=explicit-with-opt-out-operator-and-message-result]
277+
----
278+
279+
as opposed to
280+
281+
----
282+
include::{sourcedir}/primer/ConditionsSpec.groovy[tag=normal-condition-result]
283+
----
284+
285+
and you can even leave out the message, although it is not recommended, to get this:
286+
287+
[source,groovy,indent=0]
288+
----
289+
include::{sourcedir}/primer/ConditionsSpec.groovy[tag=explicit-with-opt-out-operator]
290+
----
291+
292+
will be rendered as
293+
294+
----
295+
include::{sourcedir}/primer/ConditionsSpec.groovy[tag=explicit-with-opt-out-operator-result]
296+
----
297+
298+
IMPORTANT: The `!!` must be the outermost expression, see https://groovy-lang.org/operators.html#_operator_precedence[Groovy's docs on Operator Precedence].
299+
If in doubt use parentheses, `!!(expression)`.
300+
245301
===== Exception Conditions
246302

247303
Exception conditions are used to describe that a `when` block should throw an exception. They are defined using the

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

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,13 @@ private Statement rewriteCondition(Statement conditionStat, Expression condition
596596
}
597597

598598
private Statement rewriteCondition(Expression expr, Expression message, boolean explicit) {
599+
boolean optOut = isOptOutExpression(expr);
600+
if (optOut) {
601+
expr = removeOptOutPrefix(expr);
602+
if (!explicit) {
603+
return new ExpressionStatement(expr);
604+
}
605+
}
599606
if (isSpecialCollectionCondition(expr)) { // todo check if we can better integrate it
600607
expr = transformSpecialCollectionCondition(expr);
601608
}
@@ -606,21 +613,37 @@ private Statement rewriteCondition(Expression expr, Expression message, boolean
606613
if ((Identifiers.WITH.equals(methodName) || Identifiers.VERIFY_ALL.equals(methodName))) {
607614
return surroundSpecialTryCatch(expr);
608615
}
609-
return rewriteMethodCondition(methodCallExpression, message, explicit);
616+
return rewriteMethodCondition(methodCallExpression, message, explicit, optOut);
610617
}
611618

612619
if (expr instanceof StaticMethodCallExpression)
613-
return rewriteStaticMethodCondition((StaticMethodCallExpression) expr, message, explicit);
620+
return rewriteStaticMethodCondition((StaticMethodCallExpression) expr, message, explicit, optOut);
621+
622+
return rewriteOtherCondition(expr, message, optOut);
623+
}
624+
625+
/**
626+
* Treat the nonsensical `!!` prefix as opt-out from implicit conditions
627+
*/
628+
private boolean isOptOutExpression(Expression expr) {
629+
return expr instanceof NotExpression && ((NotExpression) expr).getExpression() instanceof NotExpression;
630+
}
614631

615-
return rewriteOtherCondition(expr, message);
632+
private Expression removeOptOutPrefix(Expression expr) {
633+
return ((NotExpression)((NotExpression)expr).getExpression()).getExpression();
616634
}
617635

618-
private Statement rewriteMethodCondition(MethodCallExpression condition, Expression message, boolean explicit) {
636+
private Statement rewriteMethodCondition(MethodCallExpression condition, Expression message, boolean explicit, boolean optOut) {
619637
MethodCallExpression rewritten;
620638
int lastVariableNum;
621-
final Expression converted = convert(condition);
622-
rewritten = (MethodCallExpression) unrecord(converted);
623-
lastVariableNum = extractVariableNumber(converted);
639+
if (explicit && optOut) {
640+
rewritten = condition;
641+
lastVariableNum = -1;
642+
} else {
643+
final Expression converted = convert(condition);
644+
rewritten = (MethodCallExpression) unrecord(converted);
645+
lastVariableNum = extractVariableNumber(converted);
646+
}
624647

625648
List<Expression> args = new ArrayList<>();
626649
args.add(rewritten.getObjectExpression());
@@ -638,16 +661,21 @@ private Statement rewriteMethodCondition(MethodCallExpression condition, Express
638661
resources.getAstNodeCache().SpockRuntime_VerifyMethodCondition,
639662
condition,
640663
message,
641-
args));
664+
args, optOut), optOut);
642665
}
643666

644667
private Statement rewriteStaticMethodCondition(StaticMethodCallExpression condition, Expression message,
645-
boolean explicit) {
668+
boolean explicit, boolean optOut) {
646669
StaticMethodCallExpression rewritten;
647670
int lastVariableNum;
648-
final Expression converted = convert(condition);
649-
rewritten = (StaticMethodCallExpression) unrecord(converted);
650-
lastVariableNum = extractVariableNumber(converted);
671+
if (explicit && optOut) {
672+
rewritten = condition;
673+
lastVariableNum = -1;
674+
} else {
675+
final Expression converted = convert(condition);
676+
rewritten = (StaticMethodCallExpression) unrecord(converted);
677+
lastVariableNum = extractVariableNumber(converted);
678+
}
651679

652680
List<Expression> args = new ArrayList<>();
653681
args.add(new ClassExpression(rewritten.getOwnerType()));
@@ -665,16 +693,18 @@ private Statement rewriteStaticMethodCondition(StaticMethodCallExpression condit
665693
resources.getAstNodeCache().SpockRuntime_VerifyMethodCondition,
666694
condition,
667695
message,
668-
args));
696+
args,
697+
optOut),
698+
optOut);
669699
}
670700

671-
private Statement rewriteOtherCondition(Expression condition, Expression message) {
672-
Expression rewritten = convert(condition);
701+
private Statement rewriteOtherCondition(Expression condition, Expression message, boolean optOut) {
702+
Expression rewritten = optOut ? condition : convert(condition);
673703

674704
final Expression executeAndVerify = rewriteToSpockRuntimeCall(resources.getAstNodeCache().SpockRuntime_VerifyCondition,
675-
condition, message, singletonList(rewritten));
705+
condition, message, singletonList(rewritten), optOut);
676706

677-
return surroundWithTryCatch(condition, message, executeAndVerify);
707+
return surroundWithTryCatch(condition, message, executeAndVerify, optOut);
678708
}
679709

680710
private Expression transformSpecialCollectionCondition(Expression condition) {
@@ -725,7 +755,7 @@ private boolean isStringLikeExpression(Expression expression) {
725755
return false;
726756
}
727757

728-
private TryCatchStatement surroundWithTryCatch(Expression condition, Expression message, Expression executeAndVerify) {
758+
private TryCatchStatement surroundWithTryCatch(Expression condition, Expression message, Expression executeAndVerify, boolean optOut) {
729759
final TryCatchStatement tryCatchStatement = new TryCatchStatement(
730760
new ExpressionStatement(executeAndVerify),
731761
EmptyStatement.INSTANCE
@@ -740,7 +770,7 @@ private TryCatchStatement surroundWithTryCatch(Expression condition, Expression
740770
resources.getAstNodeCache().SpockRuntime_ConditionFailedWithException,
741771
new ArgumentListExpression(asList(
742772
new VariableExpression(errorCollectorName),
743-
new VariableExpression(valueRecorderName), // recorder
773+
optOut ? ConstantExpression.NULL : new VariableExpression(valueRecorderName), // recorder
744774
new ConstantExpression(resources.getSourceText(condition)), // text
745775
new ConstantExpression(condition.getLineNumber()), // line
746776
new ConstantExpression(condition.getColumnNumber()), // column
@@ -779,15 +809,15 @@ private TryCatchStatement surroundSpecialTryCatch(Expression executeAndVerify) {
779809
}
780810

781811
private Expression rewriteToSpockRuntimeCall(MethodNode method, Expression condition, Expression message,
782-
List<Expression> additionalArgs) {
812+
List<Expression> additionalArgs, boolean optOut) {
783813
List<Expression> args = new ArrayList<>();
784814

785815
MethodCallExpression result = createDirectMethodCall(
786816
new ClassExpression(resources.getAstNodeCache().SpockRuntime), method,
787817
new ArgumentListExpression(args));
788818

789819
args.add(new VariableExpression(errorCollectorName, resources.getAstNodeCache().ErrorCollector));
790-
args.add(createDirectMethodCall(
820+
args.add(createDirectMethodCall(optOut ? ConstantExpression.NULL :
791821
new VariableExpression(valueRecorderName),
792822
resources.getAstNodeCache().ValueRecorder_Reset,
793823
ArgumentListExpression.EMPTY_ARGUMENTS));
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.spockframework.docs.primer
2+
3+
import org.spockframework.runtime.ConditionNotSatisfiedError
4+
import org.spockframework.smoke.condition.ConditionRenderingSpec
5+
import spock.lang.FailsWith
6+
7+
class ConditionsSpec extends ConditionRenderingSpec {
8+
9+
def "normal condition rendering"() {
10+
expect:
11+
isRendered(/* tag::normal-condition-result[] */"""
12+
foo == bar
13+
| | |
14+
| | very long bar
15+
| false
16+
| 3 differences (76% similarity)
17+
| very long (foo)
18+
| very long (bar)
19+
very long foo
20+
21+
foobar
22+
""", // end::normal-condition-result[]
23+
{
24+
// tag::normal-condition[]
25+
def foo = 'very long foo'
26+
def bar = 'very long bar'
27+
assert foo == bar : "foobar"
28+
// end::normal-condition[]
29+
}
30+
)
31+
}
32+
33+
def "condition rendering can be disabled via opt-out operator"() {
34+
expect:
35+
isRendered(/* tag::explicit-with-opt-out-operator-and-message-result[] */"""
36+
(foo == bar)
37+
38+
foobar
39+
""", // end::explicit-with-opt-out-operator-and-message-result[]
40+
{
41+
// tag::explicit-with-opt-out-operator-and-message[]
42+
def foo = 'very long foo'
43+
def bar = 'very long bar'
44+
assert !!(foo == bar) : "foobar"
45+
// end::explicit-with-opt-out-operator-and-message[]
46+
}
47+
)
48+
}
49+
50+
def "condition rendering can be disabled via opt-out operator without message"() {
51+
expect:
52+
isRendered(/* tag::explicit-with-opt-out-operator-result[] */"""
53+
(foo == bar)
54+
""",// end::explicit-with-opt-out-operator-result[]
55+
{
56+
def foo = 'very long foo'
57+
def bar = 'very long bar'
58+
// tag::explicit-with-opt-out-operator[]
59+
assert !!(foo == bar)
60+
// end::explicit-with-opt-out-operator[]
61+
})
62+
}
63+
64+
@FailsWith(ConditionNotSatisfiedError)
65+
def "using each-assert fails normally"(){
66+
given:
67+
def aList = []
68+
// tag::each-assert-normal[]
69+
expect:
70+
aList.each { assert it > 0 }
71+
// end::each-assert-normal[]
72+
}
73+
74+
def "using each-assert with opt-out passes"(){
75+
given:
76+
def aList = []
77+
// tag::each-assert-opt-out-operator[]
78+
expect:
79+
!!aList.each { assert it > 0 }
80+
// end::each-assert-opt-out-operator[]
81+
}
82+
}

spock-specs/src/test/groovy/org/spockframework/smoke/condition/ConditionEvaluation.groovy

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@
1616

1717
package org.spockframework.smoke.condition
1818

19+
import org.opentest4j.AssertionFailedError
1920
import org.spockframework.EmbeddedSpecification
21+
import org.spockframework.runtime.ConditionFailedWithExceptionError
2022
import org.spockframework.runtime.ConditionNotSatisfiedError
2123

2224
import spock.lang.Issue
2325
import spock.lang.FailsWith
24-
import spock.lang.Specification
25-
26-
import java.util.concurrent.Callable
2726

2827
import static java.lang.Math.max
2928
import static java.lang.Math.min
@@ -293,7 +292,6 @@ class ConditionEvaluation extends EmbeddedSpecification {
293292
def "NotExpression"() {
294293
expect:
295294
!false
296-
!!true
297295
!(true && false)
298296
}
299297
@@ -391,6 +389,45 @@ class ConditionEvaluation extends EmbeddedSpecification {
391389
[aType, bType] << ['int[]', 'Integer[]', 'List', 'Queue', 'Deque'].with { [it, it] }.combinations()
392390
}
393391
392+
@FailsWith(ConditionFailedWithExceptionError)
393+
def "regular implicit condition"() {
394+
expect:
395+
customContains("foo", "bar")
396+
}
397+
398+
@FailsWith(AssertionFailedError)
399+
def "opt-out of implicit-condition handling does not wrap exceptions"() {
400+
expect:
401+
!!customContains("foo", "bar")
402+
}
403+
404+
def "opt-out of implicit-condition handling won't fail for falsy values"() {
405+
expect:
406+
!!aList.each { assert it > 0 }
407+
408+
where:
409+
aList << [[1], []]
410+
}
411+
412+
def "opt-out of implicit-condition handling won't fail static method conditions returning falsy value"() {
413+
expect:
414+
!!min(0, 0)
415+
}
416+
417+
@FailsWith(ConditionNotSatisfiedError)
418+
def "opt-out only works if it is the outermost expression"() {
419+
expect:
420+
!!false && false
421+
}
422+
423+
424+
def "opt-out only works for the outermost expression"() {
425+
expect:
426+
!!(true && false)
427+
!!true
428+
!!false
429+
}
430+
394431
/*
395432
def "MapEntryExpression"() {
396433
// tested as part of testMapExpression
@@ -413,6 +450,12 @@ class ConditionEvaluation extends EmbeddedSpecification {
413450
}
414451
*/
415452
453+
void customContains(String a, String b) {
454+
if(!a.contains(b)) {
455+
throw new AssertionFailedError("'$a' does not contain '$b'", a, b)
456+
}
457+
}
458+
416459
static class Properties {
417460
def getNext() { this }
418461

0 commit comments

Comments
 (0)