Skip to content
Merged
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
2 changes: 2 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ include::include.adoc[]
* Improve `@TempDir` field injection, now it happens before field initialization, so it can be used by other field initializers.
* Fix exception when configured `baseDir` was not existing, now `@TempDir` will create the baseDir directory if it is missing.
* Fix bad error message for collection conditions, when one of the operands is `null`
* Fix possible deadlock, when blocking in mock response generators spockPull:1885[]
** This fixes the issues: spockIssue:583[], spockIssue:1882[]
* Document `@ConditionBlock` Annotation spockPull:1709[]
* Document `old`-Method spockPull:1708[]
* Clarify documentation for global Mocks spockPull:1755[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,32 @@ public MockController() {
}

@Override
public synchronized Object handle(IMockInvocation invocation) {
public Object handle(IMockInvocation invocation) {
Comment thread
AndreasTu marked this conversation as resolved.
Optional<IMockInteraction> interaction = findInteraction(invocation);
if (interaction.isPresent()) {
try {
return interaction.get().accept(invocation);
} catch (InteractionNotSatisfiedError e) {
synchronized (this) {
errors.add(e);
}
throw e;
}
}
return invocation.getMockObject().getDefaultResponse().respond(invocation);
}

private synchronized Optional<IMockInteraction> findInteraction(IMockInvocation invocation) {
for (IInteractionScope scope : scopes) {
IMockInteraction interaction = scope.match(invocation);
if (interaction != null) {
try {
return interaction.accept(invocation);
} catch (InteractionNotSatisfiedError e) {
errors.add(e);
throw e;
}
return Optional.of(interaction);
}
}
for (IInteractionScope scope : scopes) {
scope.addUnmatchedInvocation(invocation);
}
return invocation.getMockObject().getDefaultResponse().respond(invocation);
return Optional.empty();
}

public static final String ADD_INTERACTION = "addInteraction";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.spockframework.util.TextUtil;

import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* An anticipated interaction between the SUT and one or more mock objects.
Expand All @@ -34,8 +35,8 @@ public class MockInteraction implements IMockInteraction {
private final int maxCount;
private final List<IInvocationConstraint> constraints;
private final IResponseGenerator responseGenerator;

private final List<IMockInvocation> acceptedInvocations = new ArrayList<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

public MockInteraction(int line, int column, String text, int minCount,
int maxCount, List<IInvocationConstraint> constraints,
Expand Down Expand Up @@ -65,12 +66,17 @@ public boolean matches(IMockInvocation invocation) {

@Override
public Object accept(IMockInvocation invocation) {
acceptedInvocations.add(invocation);
if (acceptedInvocations.size() > maxCount) {
throw new TooManyInvocationsError(this, acceptedInvocations);
}
lock.writeLock().lock();
try {
acceptedInvocations.add(invocation);
if (acceptedInvocations.size() > maxCount) {
throw new TooManyInvocationsError(this, acceptedInvocations);
}

return responseGenerator == null ? null : responseGenerator.respond(invocation);
return responseGenerator == null ? null : responseGenerator.respond(invocation);
} finally {
lock.writeLock().unlock();
}
}

@Override
Expand Down Expand Up @@ -121,12 +127,22 @@ public String describeMismatch(IMockInvocation invocation) {

@Override
public boolean isSatisfied() {
return acceptedInvocations.size() >= minCount;
lock.readLock().lock();
try {
return acceptedInvocations.size() >= minCount;
} finally {
lock.readLock().unlock();
}
}

@Override
public boolean isExhausted() {
return acceptedInvocations.size() >= maxCount;
lock.readLock().lock();
try {
return acceptedInvocations.size() >= maxCount;
} finally {
lock.readLock().unlock();
}
}

@Override
Expand All @@ -150,8 +166,11 @@ public String getText() {
}

public String toString() {
return String.format("%s (%d %s)", text, acceptedInvocations.size(), acceptedInvocations.size() == 1 ? "invocation" : "invocations");
lock.readLock().lock();
try {
return String.format("%s (%d %s)", text, acceptedInvocations.size(), acceptedInvocations.size() == 1 ? "invocation" : "invocations");
} finally {
lock.readLock().unlock();
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.spockframework.smoke.mock

import spock.lang.Issue
import spock.lang.Specification

import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

class MultiThreaded extends Specification {

@Issue("https://github.com/spockframework/spock/issues/583")
def "mocks can block and wait on conditions"() {
given:
CountDownLatch latch = new CountDownLatch(2)
List aList = Mock ()
def exec = Executors.newFixedThreadPool(2)

when:
exec.submit { aList.get(0) }
exec.submit { aList.get(0) }
exec.shutdown()
latch.await()

then:
exec.awaitTermination(30, TimeUnit.SECONDS)

2 * aList.get(_) >> {
latch.countDown()
assert latch.await(30, TimeUnit.SECONDS)
42
}
}
}