/* * Copyright 2010-2016 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 net.jodah.concurrentunit; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import net.jodah.concurrentunit.internal.ReentrantCircuit; /** * Waits on a test, carrying out assertions, until being resumed. * @author Jonathan Halterman */ public class Waiter { /** the message to show when there is a timeout of the thread. */ private static final String TIMEOUT_MESSAGE = "Test timed out while waiting for an expected result, expectedResumes: %d, actualResumes: %d"; /** the number of remaining resumes. */ private AtomicInteger remainingResumes = new AtomicInteger(0); /** the circuit ith open() and close() calls. */ private final ReentrantCircuit circuit = new ReentrantCircuit(); /** the failure when it occurs. */ private volatile Throwable failure; /** * Creates a new Waiter. */ public Waiter() { this.circuit.open(); } /** * Asserts that the {@code expected} values equals the {@code actual} value * @param expected the expected value * @param actual the actual value * @throws AssertionError when the assertion fails */ public void assertEquals(final Object expected, final Object actual) { if (expected == null && actual == null) return; if (expected != null && expected.equals(actual)) return; fail(format(expected, actual)); } /** * Asserts that the {@code expected} values equals the {@code actual} value * @param expected the expected value * @param actual the actual value * @param delta the allowed tolerance * @throws AssertionError when the assertion fails */ public void assertEquals(final double expected, final double actual, final double delta) { try { org.junit.Assert.assertEquals(expected, actual, delta); } catch (AssertionError ae) { fail(format(expected, actual)); } } /** * Asserts that the {@code condition} is false. * @param condition boolean to test to be false * @throws AssertionError when the assertion fails */ public void assertFalse(final boolean condition) { if (condition) fail("expected false"); } /** * Asserts that the {@code object} is not null. * @param object the object to test * @throws AssertionError when the assertion fails */ public void assertNotNull(final Object object) { if (object == null) fail("expected not null"); } /** * Asserts that the {@code object} is null. * @param object the object to test * @throws AssertionError when the assertion fails */ public void assertNull(final Object object) { if (object != null) fail(format("null", object)); } /** * Asserts that the {@code condition} is true. * @param condition boolean to test to be true * @throws AssertionError when the assertion fails */ public void assertTrue(final boolean condition) { if (!condition) fail("expected true"); } /** * Asserts that {@code actual} satisfies the condition specified by {@code matcher}. * @param actual the value to test * @param matcher the condition * @param the type of the actual * @throws AssertionError when the assertion fails */ public void assertThat(final T actual, final org.hamcrest.Matcher matcher) { try { org.hamcrest.MatcherAssert.assertThat(actual, matcher); } catch (AssertionError e) { fail(e); } } /** * Waits until {@link #resume()} is called, or the test is failed. * @throws TimeoutException if the operation times out while waiting * @throws InterruptedException if the operations is interrupted while waiting * @throws AssertionError if any assertion fails while waiting */ public void await() throws TimeoutException, InterruptedException { await(0, TimeUnit.MILLISECONDS, 1); } /** * Waits until the {@code delay} has elapsed, {@link #resume()} is called, or the test is failed. * @param delay Delay to wait in milliseconds * @throws TimeoutException if the operation times out while waiting * @throws InterruptedException if the operations is interrupted while waiting * @throws AssertionError if any assertion fails while waiting */ public void await(final long delay) throws TimeoutException, InterruptedException { await(delay, TimeUnit.MILLISECONDS, 1); } /** * Waits until the {@code delay} has elapsed, {@link #resume()} is called, or the test is failed. * @param delay Delay to wait for * @param timeUnit TimeUnit to delay for * @throws TimeoutException if the operation times out while waiting * @throws InterruptedException if the operations is interrupted while waiting * @throws AssertionError if any assertion fails while waiting */ public void await(final long delay, final TimeUnit timeUnit) throws TimeoutException, InterruptedException { await(delay, timeUnit, 1); } /** * Waits until the {@code delay} has elapsed, {@link #resume()} is called {@code expectedResumes} times, or the test is * failed. * @param delay Delay to wait for in milliseconds * @param expectedResumes Number of times {@link #resume()} is expected to be called before the awaiting thread is resumed * @throws TimeoutException if the operation times out while waiting * @throws InterruptedException if the operations is interrupted while waiting * @throws AssertionError if any assertion fails while waiting */ public void await(final long delay, final int expectedResumes) throws TimeoutException, InterruptedException { await(delay, TimeUnit.MILLISECONDS, expectedResumes); } /** * Waits until the {@code delay} has elapsed, {@link #resume()} is called {@code expectedResumes} times, or the test is * failed. * @param delay Delay to wait for * @param timeUnit TimeUnit to delay for * @param expectedResumes Number of times {@link #resume()} is expected to be called before the awaiting thread is resumed * @throws TimeoutException if the operation times out while waiting * @throws InterruptedException if the operations is interrupted while waiting * @throws AssertionError if any assertion fails while waiting */ public void await(final long delay, final TimeUnit timeUnit, final int expectedResumes) throws TimeoutException, InterruptedException { try { if (this.failure == null) { synchronized (this) { int remaining = this.remainingResumes.addAndGet(expectedResumes); if (remaining > 0) this.circuit.open(); } if (delay == 0) this.circuit.await(); else if (!this.circuit.await(delay, timeUnit)) { final int actualResumes = expectedResumes - this.remainingResumes.get(); throw new TimeoutException(String.format(TIMEOUT_MESSAGE, expectedResumes, actualResumes)); } } } finally { this.remainingResumes.set(0); this.circuit.open(); if (this.failure != null) { Throwable f = this.failure; this.failure = null; sneakyThrow(f); } } } /** * Resumes the waiter when the expected number of {@link #resume()} calls have occurred. */ public synchronized void resume() { if (this.remainingResumes.decrementAndGet() <= 0) this.circuit.close(); } /** * Fails the current test. * @throws AssertionError as a result of failure */ public void fail() { fail(new AssertionError()); } /** * Fails the current test for the given {@code reason}. * @param reason the reason for failure * @throws AssertionError wrapping the reason */ public void fail(final String reason) { fail(new AssertionError(reason)); } /** * Fails the current test with the given {@code reason}, sets the number of expected resumes to 0, and throws the * {@code reason} as an {@code AssertionError} in the main test thread and in the current thread. * @param reason the reason for failure * @throws AssertionError wrapping the {@code reason} */ public void fail(final Throwable reason) { AssertionError ae = null; if (reason instanceof AssertionError) ae = (AssertionError) reason; else { ae = new AssertionError(); ae.initCause(reason); } this.failure = ae; this.circuit.close(); throw ae; } /** * Rethrows the {@code failure} in the main test thread and in the current thread. Differs from {@link #fail(Throwable)} * which wraps a failure in an AssertionError before throwing. * @param failure the reason for failure * throws Throwable the {@code failure} */ @SuppressWarnings("hiding") public void rethrow(Throwable failure) { this.failure = failure; this.circuit.close(); sneakyThrow(failure); } /** * Throw the throwable without having to specify it in the signature. * @param t the throwable */ private static void sneakyThrow(Throwable t) { Waiter. sneakyThrow2(t); } /** * Throw the throwable T. */ @SuppressWarnings("unchecked") private static void sneakyThrow2(final Throwable t) throws T { throw (T) t; } /** * Format the expected - actual string of JUnit. * @param expected the expected value * @param actual the actual value * @return a formatted string */ private String format(final Object expected, final Object actual) { return "expected:<" + expected + "> but was:<" + actual + ">"; } }