/* * Copyright 2012 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 groovy.lang.Closure; import org.spockframework.lang.ConditionBlock; import org.spockframework.runtime.GroovyRuntimeUtil; import org.spockframework.runtime.SpockTimeoutError; import org.spockframework.util.Beta; /** * Repeatedly evaluates one or more conditions until they are satisfied or a timeout has elapsed. * The timeout and delays between evaluation attempts are configurable. All durations are in seconds. * * <p>Usage example:</p> * * <pre> * def conditions = new PollingConditions(timeout: 10, initialDelay: 1.5, factor: 1.25) * def machine = new Machine() * * when: * machine.start() * * then: * conditions.eventually { * assert machine.temperature >= 100 * assert machine.efficiency >= 0.9 * } * </pre> */ @Beta public class PollingConditions { private double timeout = 1; private double initialDelay = 0; private double delay = 0.1; private double factor = 1.0; /** * Returns the timeout (in seconds) until which the conditions have to be satisfied. * Defaults to one second. * * @return the timeout (in seconds) until which the conditions have to be satisfied */ public double getTimeout() { return timeout; } /** * Sets the timeout (in seconds) until which the conditions have to be satisfied. * Defaults to one second. * * @param seconds the timeout (in seconds) until which the conditions have to be satisfied */ public void setTimeout(double seconds) { timeout = seconds; } /** * Returns the initial delay (in seconds) before first evaluating the conditions. * Defaults to zero seconds. */ public double getInitialDelay() { return initialDelay; } /** * Sets the initial delay (in seconds) before first evaluating the conditions. * Defaults to zero seconds. * * @param seconds the initial delay (in seconds) before first evaluating the conditions */ public void setInitialDelay(double seconds) { initialDelay = seconds; } /** * Returns the delay (in seconds) between successive evaluations of the conditions. * Defaults to 0.1 seconds. */ public double getDelay() { return delay; } /** * Sets the delay (in seconds) between successive evaluations of the conditions. * Defaults to 0.1 seconds. * * @param seconds the delay (in seconds) between successive evaluations of the conditions. */ public void setDelay(double seconds) { this.delay = seconds; } /** * Returns the factor by which the delay grows (or shrinks) after each evaluation of the conditions. * Defaults to 1. */ public double getFactor() { return factor; } /** * Sets the factor by which the delay grows (or shrinks) after each evaluation of the conditions. * Defaults to 1. * * @param factor the factor by which the delay grows (or shrinks) after each evaluation of the conditions */ public void setFactor(double factor) { this.factor = factor; } /** * Repeatedly evaluates the specified conditions until they are satisfied or the timeout has elapsed. * * @param conditions the conditions to evaluate * * @throws InterruptedException if evaluation is interrupted */ @ConditionBlock public void eventually(Closure<?> conditions) throws InterruptedException { within(timeout, conditions); } /** * Repeatedly evaluates the specified conditions until they are satisfied or the specified timeout (in seconds) has elapsed. * * @param conditions the conditions to evaluate * * @throws InterruptedException if evaluation is interrupted */ @ConditionBlock public void within(double seconds, Closure<?> conditions) throws InterruptedException { long timeoutMillis = toMillis(seconds); long start = System.currentTimeMillis(); long lastAttempt = 0; Thread.sleep(toMillis(initialDelay)); long currDelay = toMillis(delay); int attempts = 0; while(true) { try { attempts++; lastAttempt = System.currentTimeMillis(); GroovyRuntimeUtil.invokeClosure(conditions); return; } catch (Throwable e) { long elapsedTime = lastAttempt - start; if (elapsedTime >= timeoutMillis) { String msg = String.format("Condition not satisfied after %1.2f seconds and %d attempts", elapsedTime / 1000d, attempts); throw new SpockTimeoutError(seconds, msg, e); } final long timeout = Math.min(currDelay, start + timeoutMillis - System.currentTimeMillis()); if (timeout > 0) { Thread.sleep(timeout); } currDelay *= factor; } } } /** * Alias for {@link #eventually(groovy.lang.Closure)}. */ @ConditionBlock public void call(Closure<?> conditions) throws InterruptedException { eventually(conditions); } /** * Alias for {@link #within(double, groovy.lang.Closure)}. */ @ConditionBlock public void call(double seconds, Closure<?> conditions) throws InterruptedException { within(seconds, conditions); } private long toMillis(double seconds) { return (long) (seconds * 1000); } }