/* * Copyright 2010 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package spock.util.concurrent; import java.util.concurrent.*; import org.spockframework.lang.ConditionBlock; import org.spockframework.runtime.SpockTimeoutError; import org.spockframework.util.ThreadSafe; import org.spockframework.util.TimeUtil; /** * Alternative to class <tt>BlockingVariable(s)</tt> that allows to evaluate conditions * in a thread other than the spec runner's thread(s). Rather than transferring * state to an expect- or then-block, it is verified right where it is captured. * On the upside, this can result in a more informative stack trace if the * evaluation of a condition fails. On the downside, the coordination between * threads is more explicit (number of <tt>evaluate</tt> blocks has to be specified if * greater than one), and the usual structure of a feature method cannot be * preserved (conditions are no longer located in expect-/then-block). * * <p>Example: * <pre> * // create object under specification * def machine = new Machine() * * def conds = new AsyncConditions() * * // register async callback * machine.workDone << { result -> * conds.evaluate { * assert result == WorkResult.OK * // could add more explicit conditions here * } * } * * when: * machine.start() * * then: * // wait for the evaluation to complete * // any exception thrown in the evaluate block will be rethrown from this method * conds.await() * * cleanup: * // shut down all threads * machine?.shutdown() * <pre> * * @author Peter Niederwieser */ @ThreadSafe public class AsyncConditions { private final int numEvalBlocks; private final CountDownLatch latch; private final ConcurrentLinkedQueue<Throwable> exceptions = new ConcurrentLinkedQueue<Throwable>(); /** * Same as <tt>AsyncConditions(1)</tt>. */ public AsyncConditions() { this(1); } /** * Instantiates an <tt>AsyncConditions</tt> instance with the specified number * of evaluate blocks. <tt>await()</tt> will block until all evaluate blocks * have completed or a timeout expires. * * <p>Note: One evaluate block may contain multiple conditions. * * @param numEvalBlocks the number of evaluate blocks that <tt>await()</tt> * should wait for */ public AsyncConditions(int numEvalBlocks) { this.numEvalBlocks = numEvalBlocks; latch = new CountDownLatch(numEvalBlocks); } /** * Evaluates the specified block, which is expected to contain * one or more explicit conditions (i.e. assert statements). * Any caught exception will be rethrown from <tt>await()</tt>. * * @param block the code block to evaluate */ @ConditionBlock public void evaluate(Runnable block) { try { block.run(); } catch (Throwable t) { exceptions.add(t); wakeUp(); } latch.countDown(); } private void wakeUp() { // this will definitely wake us up; it doesn't matter that countDown() // might get called too often long pendingEvalBlocks = latch.getCount(); for (int i = 0; i < pendingEvalBlocks; i++) latch.countDown(); } /** * Same as {@code await(1)}. */ public void await() throws InterruptedException, Throwable { await(1); } /** * Waits until all evaluate blocks have completed or the specified timeout * (in seconds) expires. If one of the evaluate blocks throws an exception, it is rethrown * from this method. * * @param seconds the timeout (in seconds) * @throws InterruptedException if the calling thread is interrupted * @throws Throwable the first exception thrown by an evaluate block */ public void await(double seconds) throws InterruptedException, Throwable { latch.await((long) (seconds * 1000), TimeUnit.MILLISECONDS); if (!exceptions.isEmpty()) throw exceptions.poll(); long pendingEvalBlocks = latch.getCount(); if (pendingEvalBlocks > 0) { String msg = String.format("Async conditions timed out " + "after %1.2f seconds; %d out of %d evaluate blocks did not complete in time", seconds, pendingEvalBlocks, numEvalBlocks); throw new SpockTimeoutError(seconds, msg); } } /** * Waits until all evaluate blocks have completed or the specified timeout * expires. If one of the evaluate blocks throws an exception, it is rethrown * from this method. * * @throws InterruptedException if the calling thread is interrupted * * @throws Throwable the first exception thrown by an evaluate block * * @deprecated use {@link #await(double)} instead */ @Deprecated public void await(int value, TimeUnit unit) throws InterruptedException, Throwable { await(TimeUtil.toSeconds(value, unit)); } }