Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ include::include.adoc[]

== 2.5 (tbd)

=== Breaking Changes

* It is no longer true that only top-level expression statements are treated as implicit conditions.
Just like within `with { ... }`, `verifyAll { ... }`, `@ConditionBlock` methods, and other places,
each expression statement is now considered an implicit condition, except for statements in unknown
closures.
Comment on lines +11 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Scope this note to assertion contexts.

The implementation does not make expression statements implicit globally; DeepBlockRewriter.handleImplicitCondition() still limits this to then/expect/filter, known condition blocks/methods, and first-level interaction closures. As written, “each expression statement” reads broader than the compiler change actually is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/release_notes.adoc` around lines 11 - 14, The release note is too broad;
change the wording to scope the behavior to assertion/condition contexts only by
clarifying that expression statements are treated as implicit conditions only in
assertion-related contexts handled by DeepBlockRewriter.handleImplicitCondition
— specifically in then/expect/filter blocks, known condition blocks/methods
(e.g., `@ConditionBlock`, verifyAll/with), and first-level interaction closures,
and not globally for all expression statements or unknown closures.

+
This enables the usage of `if` and similar while also being able to still safely use `.every { ... }`,
but without the regular confusion why a test is not failing if the condition is inside an `if`.
You can even have interactions in those nested places now.
+
As now expression statements are considered implicit conditions that previously were not,
this can lead to compilation errors in existing code-bases, because such nested statements were
previously simply ignored by the Spock compiler and now also must be valid conditions.
+
If those compile errors are not accidentally written assignments that should have been conditions
or similar, you can for example use the <<spock_primer.adoc#opt-out-of-condition-handling,`!!` prefix operator>>
to declare an expression statement explicitly as not being an implicit condition.
+
spockPull:2250[]

=== Enhancements

* Improve `TooManyInvocationsError` now reports unsatisfied interactions with argument mismatch details, making it easier to diagnose why invocations didn't match expected interactions spockPull:2315[]
Expand Down
5 changes: 3 additions & 2 deletions docs/spock_primer.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,9 @@ digestible form. Nice, isn't it?
===== Implicit and explicit conditions

Conditions are an essential ingredient of `then` blocks and `expect` blocks. Except for calls to `void` methods and
expressions classified as interactions, all top-level expressions in these blocks are implicitly treated as conditions.
To use conditions in other places, you need to designate them with Groovy's assert keyword:
expressions classified as interactions, all expression statements in these blocks are implicitly treated as conditions
except if they happen inside some unknown closure.
To use conditions in other places, you need to designate them with Groovy's `assert` keyword:

[source,groovy]
----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public final void visitClosureExpression(ClosureExpression expr) {
groupConditionFound = false;
ISpecialMethodCall oldSpecialMethodCall = currSpecialMethodCall;
if (!currSpecialMethodCall.isMatch(expr)) {
currSpecialMethodCall = NoSpecialMethodCall.INSTANCE; // unrelated closure terminates currSpecialMethodCall scope
currSpecialMethodCall = NoSpecialMethodCall.CLOSURE_INSTANCE; // unrelated closure terminates currSpecialMethodCall scope
}
try {
Statement code = expr.getCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ private boolean handleInteraction(InteractionRewriter rewriter, ExpressionStatem
}

private boolean handleImplicitCondition(ExpressionStatement stat) {
if (!(stat == currTopLevelStat && isThenOrExpectOrFilterBlock()
if (!(((currSpecialMethodCall == NoSpecialMethodCall.INSTANCE) && isThenOrExpectOrFilterBlock())
|| currSpecialMethodCall.isConditionMethodCall()
|| currSpecialMethodCall.isConditionBlock()
|| currSpecialMethodCall.isGroupConditionBlock()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

public class NoSpecialMethodCall implements ISpecialMethodCall {
public static final ISpecialMethodCall INSTANCE = new NoSpecialMethodCall();
public static final ISpecialMethodCall CLOSURE_INSTANCE = new NoSpecialMethodCall();

@Override
public boolean isMethodName(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class JUnitFixtureMethods extends JUnitBaseSpec {
then:
def e = result.failures[exceptionPos].exception
if (suppressed) {
e = e.suppressed[0]
!!(e = e.suppressed[0])
}
e instanceof RuntimeException
e.message == name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ class ConditionEvaluation extends EmbeddedSpecification {
7
}

@FailsWith(ConditionNotSatisfiedError)
def "failing non-top-level condition"() {
expect:
if (true) {
2 * 3 == 7
}
}

def "failing non-top-level non-condition"() {
expect:
[1, 2, 3].any { it == 2 }
}

def "MethodCallExpression"() {
expect:
[1, 2, 3].size() == 3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,12 @@ class TempDirExtensionSpec extends EmbeddedSpecification {
def tempFile = aaabbb.resolve("tmp.txt")
Files.createDirectories(aaabbb)
Files.write(tempFile, "ewfwf".bytes)
aaabbb.toFile().writable = false
aaa.toFile().writable = false
tempFile.toFile().writable = false
previousIteration = iterationDir
!!(aaabbb.toFile().writable = false)
!!(aaa.toFile().writable = false)
!!(tempFile.toFile().writable = false)
!!(previousIteration = iterationDir)
} else if (i == 1) {
assert !previousIteration.exists()
!previousIteration.exists()
}

where:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,15 @@ public void $spock_feature_0_0() {
finally {
}
if (true) {
[true, false].any({ ->
it
})
try {
org.spockframework.runtime.SpockRuntime.verifyMethodCondition($spock_errorCollector, $spock_valueRecorder.reset(), '[true, false].any { it }', 5, 9, null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(2), [$spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(0), true), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(1), false)]), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(3), 'any'), new java.lang.Object[]{$spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(4), { ->
it
})}, $spock_valueRecorder.realizeNas(7, false), false, 6)
}
catch (java.lang.Throwable $spock_condition_throwable) {
org.spockframework.runtime.SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, '[true, false].any { it }', 5, 9, null, $spock_condition_throwable)}
finally {
}
}
this.with({ ->
org.spockframework.runtime.ValueRecorder $spock_valueRecorder1 = new org.spockframework.runtime.ValueRecorder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,15 @@ public void $spock_feature_0_0() {
finally {
}
if (true) {
[true, false].any({ ->
it
})
try {
org.spockframework.runtime.SpockRuntime.verifyMethodCondition($spock_errorCollector, $spock_valueRecorder.reset(), '[true, false].any { it }', 5, 9, null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(2), [$spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(0), true), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(1), false)]), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(3), 'any'), new java.lang.Object[]{$spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(4), { ->
it
})}, $spock_valueRecorder.realizeNas(7, false), false, 6)
}
catch (java.lang.Throwable $spock_condition_throwable) {
org.spockframework.runtime.SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, '[true, false].any { it }', 5, 9, null, $spock_condition_throwable)}
finally {
}
}
this.with({ ->
org.spockframework.runtime.ValueRecorder $spock_valueRecorder1 = new org.spockframework.runtime.ValueRecorder()
Expand Down
Loading