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

Add documentation for concurrency helpers #1173

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
88 changes: 88 additions & 0 deletions docs/utilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,91 @@ include::{sourcedir}/utilities/MutableClockDocSpec.groovy[tag=age-filter-spec]

There are many more ways to modify `MutableClock` just have a look at the JavaDocs, or the test code `spock.util.time.MutableClockSpec`.

[[async-conditions]]
== Evaluating conditions asynchronously with `AsyncConditions`

The utility class `AsyncConditions` can be helpful when working with asynchronous assertions. These are assertions made
from a different thread than the one running the test, for example when working with callback functions.
The individual assertions are collected using the `evaluate` method, and in the asserting block, e.g. `then`, the
`await` method is called to verify the results, blocking until a timeout expires, and failing if any of the evaluations
failed.
The timeout in seconds can be specified as a parameter, see the example below. The default is `1.0` seconds.
Additionally, the number of expected evaluations must be provided initially, the default value is `1`.

NOTE: In order to use implicit assertions, a typed instance of this class must be declared. When declaring an untyped
instance (using the `def` keyword), it is required to explicitly `assert` all statements inside the block (see example
below).

=== Example

.Test
[source,groovy,indent=0]
----
include::{sourcedir}/utilities/concurrent/AsyncConditionsDocSpec.groovy[tag=async-conditions-spec]
----
<1> create a default `AsyncConditions` object (expecting a single evaluation)
<2> A new thread is created, and code is passed to be run from it. This could also happen implicitly, when working with
a method expecting a callback parameter.
<3> Pass any code that wants to do assertions to the `evaluate` method. Because we declared the instance of
`AsyncConditions` using the `def` keyword, we have to use `assert` explicitly.
<4> finally, call the `await` method

<5> Create an `AsyncConditions` object (expecting three evaluations). This time we are using a statically typed
instance.
<6> Call `evaluate` multiple times. Using a typed instance above allows using implicit assertions (no `assert` keyword).
keyword.
<7> call `await` in the end, specifying 2.5 seconds as the timeout

[[polling-conditions]]
== Polling until conditions are fulfilled with `PollingConditions`

The `PollingConditions` utility can be used to (repeatedly) check on asynchronous conditions. The difference compared to
`AsyncConditions` is that the result is obtained passively, there is no `await` method.
`PollingConditions` allows to configure the timeout, delay and a delay increasing factor for the checks. Any number of
code blocks are passed to the `eventually` method, which will evaluate them, according to given time parameters, and
either pass or throw one of the appropriate Spock exceptions.
The `within` method can be used to override the timeout for a single invocation. See the second example below for a more
complicated use case.

NOTE: The same restriction when using the `def` keyword exists here also, see the note on `AsyncConditions`

=== Example

.Test
[source,groovy,indent=0]
----
include::{sourcedir}/utilities/concurrent/PollingConditionsDocSpec.groovy[tag=polling-conditions-spec]
----
<1> create a `PollingConditions` object
<2> values are set to variables in an asynchronous way
<3> block(s) of code containing assertions, passed in a `when` step
<4> verify the result by checking for exceptions
<5> Override the timeout for a single invocation. Using `expect` (or `then`) here forces immediate evaluation.
<6> only the first failed evaluation will be reported

[[blocking-variables]]
== Evaluating asynchronous variables with `BlockingVariable` and `BlockingVariables`

The two utility classes `BlockingVariable` and `BlockingVariables` are there to help with collecting variables that are
Copy link
Member

Choose a reason for hiding this comment

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

Since Spock 2.0 uses Java 8 BlockingVariable is kind of obsolete as there is CompletableFuture, we are still debating whether to deprecate it or remove it entirely.

Copy link
Author

Choose a reason for hiding this comment

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

And this should be mentioned in the docs?

Copy link
Member

Choose a reason for hiding this comment

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

No, but if the class is going to be deprecated or deleted it does not need to be documented. ;-)

written asynchronously, e.g. from another thread. Reading the variable(s) will block the current thread until a value is
available and returned or the timeout is reached.
For a single variable, a type parameter can be provided, see the example below.
An instance of `BlockingVariables` allows setting properties to it dynamically, and comparisons (i.e. assertions) can be
made for the individual properties, accessing them by their name.
The maximum amount of seconds to wait for can be specified as a parameter, the default is `1.0` seconds.

thokari marked this conversation as resolved.
Show resolved Hide resolved
=== Example

.Test
[source,groovy,indent=0]
----
include::{sourcedir}/utilities/concurrent/BlockingVariablesDocSpec.groovy[tag=blocking-variables-spec]
----
<1> create a `BlockingVariable` object, providing an optional type parameter
<2> A new thread is created, and immediately put to sleep for some time. Afterwards it will set the value.
<3> The value is accessed via the `get` method. This happens immediately, as the main thread is never interrupted. The
method will block until the value is available.

<4> create a `BlockingVariables` object for multiple variables, providing an optional timeout
<5> set values for `foo`, `bar` and `baz` at some point in the future
<6> compare the individual properties, blocking this thread until they are available
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.spockframework.docs.utilities.concurrent

import spock.lang.Specification
import spock.util.concurrent.AsyncConditions

class AsyncConditionsDocSpec extends Specification {

// tag::async-conditions-spec[]
def "example of single passing explicit evaluation"() {
def conditions = new AsyncConditions() // <1>
thokari marked this conversation as resolved.
Show resolved Hide resolved

when:
Thread.start { // <2>
conditions.evaluate { //<3>
assert true
}
}

then:
conditions.await() // <4>
}

def "example of multiple passing implicit evaluations"() {
AsyncConditions conditions = new AsyncConditions(3) // <5>

when:
Thread.start {
conditions.evaluate { // <6>
true
true
}
conditions.evaluate { // <6>
true
}
}

and:
Thread.start {
conditions.evaluate { // <6>
true
}
}

then:
conditions.await(2.5) // <7>
}
// end::async-conditions-spec[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.spockframework.docs.utilities.concurrent

import spock.lang.Specification
import spock.util.concurrent.BlockingVariable
import spock.util.concurrent.BlockingVariables

class BlockingVariablesDocSpec extends Specification {

// tag::blocking-variables-spec[]
def "single variable is read before it is written"() {
def list = new BlockingVariable<List<Integer>>() // <1>

when:
Thread.start {
Thread.sleep(250) // <2>
println "calling set"
list.set([1, 2, 3])
}

then:
println "calling get, blocking"
list.get() == [1, 2, 3] // <3>
}

def "example of multiple variables"() {
def vars = new BlockingVariables(2.0) // <4>

when:
Thread.start {
Thread.sleep(500) // <5>
Copy link
Member

Choose a reason for hiding this comment

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

Please use shorter sleeps as this slows down our overall tests, i.e. 100 or less

Copy link
Author

Choose a reason for hiding this comment

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

I made some tests, i.e. repeating the specs ~50 times, and it seemed that (on my machine) the sleeps could go as low as about 10ms before the order of execution for some of the statements would be changing. So I chose 25ms and 50ms respectively in the commit I just made.
Mind that there are also some timeouts in the actual tests, i.e. in the spock.util.concurrent package, which is where I got the numbers from in the first place. I considered lowering them also, but didn't want to without your approval. Let me know, and I will change those numbers also.

Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately, the CI servers are slower than most PCs so that is why there is a bit of a margin in the BlockingVariable(s)Spec. It could probably be lowered to 250 without making the tests too flaky on ci.

However, if the time is not really critical to the test it should be as low possible. In these tests it is just used to visualize a delay, the correctness of BlockingVariable is already tested in the other specs.

println "setting bar and baz"
vars.bar = 2
vars.baz = 3
}
Thread.start {
Thread.sleep(250) // <5>
println "setting foo"
vars.foo = 1
}

then:
println "before comparison, blocking"
vars.foo == 1 // <6>
vars.bar == 2
vars.baz == 3
}
// end::blocking-variables-spec[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.spockframework.docs.utilities.concurrent

import org.spockframework.runtime.ConditionNotSatisfiedError
import org.spockframework.runtime.SpockTimeoutError
import spock.lang.Specification
import spock.util.concurrent.PollingConditions

class PollingConditionsDocSpec extends Specification {
// tag::polling-conditions-spec[]
PollingConditions conditions = new PollingConditions() // <1>

volatile int num = 0
volatile String str = null

def "time controls and their default values"() {
expect:
with(conditions) {
timeout == 1
initialDelay == 0
delay == 0.1
factor == 1
}
}

def "succeeds if all conditions are eventually satisfied"() {

when:
Thread.start {
num = 42
sleep(500) // <2>
str = "hello"
}

then:
conditions.eventually { // <3>
num == 42
}

and:
conditions.eventually { // <3>
str == "hello"
}

and:
noExceptionThrown() // <4>
}

def "fails if any condition isn't satisfied in time"() {

given:
Thread.start {
num = 42
sleep(500) // milliseconds <2>
str = "hello"
}

expect:
conditions.within(0.1) { // seconds <5>
num == 42
}

when:
conditions.eventually { // <3>
num == 0
str == "bye"
}

then:
def error = thrown(SpockTimeoutError)
error.cause.message.contains('num == 0') // <6>
}
// end::polling-conditions-spec[]
}