/* * Copyright 2015 Mark Michaelis * * 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 com.github.mmichaelis.hamcrest.nextdeed.concurrent; import static com.github.mmichaelis.hamcrest.nextdeed.glue.DescribedFunction.describe; import static org.junit.Assert.assertThat; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import org.hamcrest.Matchers; import org.jetbrains.annotations.Nullable; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ErrorCollector; import org.junit.rules.ExpectedException; import org.junit.rules.TestName; import org.mockito.AdditionalAnswers; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mockito; import org.mockito.internal.verification.VerificationModeFactory; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; import java.util.List; import java.util.concurrent.TimeUnit; /** * Tests {@link WaitFunction}. * * @since 1.0.0 */ public class WaitFunctionTest { @Rule public TestName testName = new TestName(); @Rule public ExpectedException expectedException = ExpectedException.none(); @Rule public ErrorCollector errorCollector = new ErrorCollector(); @Test public void without_predicate_accept_any_value_immediately() throws Exception { WaitFunction<Void, String> waitFunction = (WaitFunction<Void, String>) WaitFunction .waitFor(new Function<Void, String>() { @Override public String apply(Void input) { return testName.getMethodName(); } }) .get(); WaitFunction<Void, String> spy = Mockito.spy(waitFunction); String result = spy.apply(null); assertThat(result, Matchers.equalTo(testName.getMethodName())); // Sleep not expected because of immediate success. Mockito.verify(spy, VerificationModeFactory.atMost(0)).sleep(Mockito.anyLong()); } @SuppressWarnings("ConstantConditions") @Test public void without_on_timeout_fail_with_timeout_exception() throws Exception { String inputValue = "Lorem"; String functionName = "Ipsum"; WaitFunction<String, String> waitFunction = (WaitFunction<String, String>) WaitFunction .waitFor(describe(new Function<String, String>() { @Override public String apply(String input) { return testName.getMethodName(); } }).as(functionName)) .toFulfill(Predicates.<String>alwaysFalse()) .within(0L, TimeUnit.MILLISECONDS) .get(); WaitFunction<String, String> spy = Mockito.spy(waitFunction); Mockito.doNothing().when(spy).sleep(Mockito.anyLong()); expectedException.expect(WaitTimeoutException.class); expectedException.expectMessage(testName.getMethodName()); expectedException.expectMessage(inputValue); expectedException.expectMessage(functionName); spy.apply(inputValue); } @Test public void pass_on_second_try() throws Exception { final Deque<Boolean> predicateAnswers = new ArrayDeque<>(Arrays.asList(false, true)); String inputValue = "Lorem"; final String outputValue = testName.getMethodName(); String functionName = "Ipsum"; WaitFunction<String, String> waitFunction = (WaitFunction<String, String>) WaitFunction .waitFor(describe(new Function<String, String>() { @Override public String apply(String input) { return outputValue; } }).as(functionName)) .toFulfill(new Predicate<String>() { @Override public boolean apply(String input) { return predicateAnswers.pop(); } }) .within(1000L, TimeUnit.MILLISECONDS) .get(); WaitFunction<String, String> spy = Mockito.spy(waitFunction); Mockito.doNothing().when(spy).sleep(Mockito.anyLong()); Mockito.doReturn(0L).when(spy).nowMillis(); String result = spy.apply(inputValue); assertThat(result, Matchers.equalTo(testName.getMethodName())); Mockito.verify(spy, VerificationModeFactory.times(1)).sleep(Mockito.anyLong()); } @Test public void adopt_new_delay_on_long_duration() throws Exception { final Deque<Boolean> predicateAnswers = new ArrayDeque<>(Arrays.asList(false, true)); String inputValue = "Lorem"; final String outputValue = testName.getMethodName(); String functionName = "Ipsum"; List<Long> timeMillis = Arrays.asList( // used to determine deadline 0L, // used to determine start time 0L, // used to evaluate time after evaluation 10L); WaitFunction<String, String> waitFunction = (WaitFunction<String, String>) WaitFunction .waitFor(describe(new Function<String, String>() { @Override public String apply(String input) { return outputValue; } }).as(functionName)) .toFulfill(new Predicate<String>() { @Override public boolean apply(String input) { return predicateAnswers.pop(); } }) .withInitialDelay(1, TimeUnit.MILLISECONDS) .within(1000L, TimeUnit.MILLISECONDS) .get(); WaitFunction<String, String> spy = Mockito.spy(waitFunction); Mockito.doNothing().when(spy).sleep(Mockito.anyLong()); Mockito.doAnswer(AdditionalAnswers.returnsElementsOf(timeMillis)).when(spy).nowMillis(); spy.apply(inputValue); Mockito.verify(spy, VerificationModeFactory.times(1)).sleep(10L); } @Test public void decelerate_polling_on_repetitive_calls() throws Exception { final Deque<Boolean> predicateAnswers = new ArrayDeque<>(Arrays.asList(false, false, true)); String inputValue = "Lorem"; final String outputValue = testName.getMethodName(); String functionName = "Ipsum"; List<Long> timeMillis = Arrays.asList( // used to determine deadline 0L, // used to determine start time 0L, // used to evaluate time after evaluation 10L, // next time before evaluation 10L, // next time after evaluation 20L); WaitFunction<String, String> waitFunction = (WaitFunction<String, String>) WaitFunction .waitFor(describe(new Function<String, String>() { @Override public String apply(String input) { return outputValue; } }).as(functionName)) .toFulfill(new Predicate<String>() { @Override public boolean apply(String input) { return predicateAnswers.pop(); } }) .withInitialDelay(1, TimeUnit.MILLISECONDS) .within(1000L, TimeUnit.MILLISECONDS) .and() .deceleratePollingBy(1.5) .get(); WaitFunction<String, String> spy = Mockito.spy(waitFunction); Mockito.doNothing().when(spy).sleep(Mockito.anyLong()); Mockito.doAnswer(AdditionalAnswers.returnsElementsOf(timeMillis)).when(spy).nowMillis(); spy.apply(inputValue); ArgumentCaptor<Long> argument = ArgumentCaptor.forClass(Long.class); InOrder inOrder = Mockito.inOrder(spy); inOrder.verify(spy).sleep(argument.capture()); inOrder.verify(spy).sleep(argument.capture()); assertThat("Delays accelerate (thus polling decelerates).", argument.getAllValues(), Matchers.equalTo(Arrays.asList(10L, 15L))); } @Test public void at_least_decelerate_by_one_millisecond() throws Exception { final Deque<Boolean> predicateAnswers = new ArrayDeque<>(Arrays.asList(false, false, true)); String inputValue = "Lorem"; final String outputValue = testName.getMethodName(); String functionName = "Ipsum"; List<Long> timeMillis = Arrays.asList( // used to determine deadline 0L, // used to determine start time 0L, // used to evaluate time after evaluation 1L, // next time before evaluation 1L, // next time after evaluation 2L); WaitFunction<String, String> waitFunction = (WaitFunction<String, String>) WaitFunction .waitFor(describe(new Function<String, String>() { @Override public String apply(String input) { return outputValue; } }).as(functionName)) .toFulfill(new Predicate<String>() { @Override public boolean apply(String input) { return predicateAnswers.pop(); } }) .withInitialDelay(1, TimeUnit.MILLISECONDS) .within(1000L, TimeUnit.MILLISECONDS) .and() .deceleratePollingBy(1.1d) .get(); WaitFunction<String, String> spy = Mockito.spy(waitFunction); Mockito.doNothing().when(spy).sleep(Mockito.anyLong()); Mockito.doAnswer(AdditionalAnswers.returnsElementsOf(timeMillis)).when(spy).nowMillis(); spy.apply(inputValue); ArgumentCaptor<Long> argument = ArgumentCaptor.forClass(Long.class); InOrder inOrder = Mockito.inOrder(spy); inOrder.verify(spy).sleep(argument.capture()); inOrder.verify(spy).sleep(argument.capture()); assertThat("Delays accelerate (thus polling decelerates) by at least one millisecond.", argument.getAllValues(), Matchers.equalTo(Arrays.asList(1L, 2L))); } @Test public void fail_on_interrupt_during_sleep() throws Exception { final Deque<Boolean> predicateAnswers = new ArrayDeque<>(Arrays.asList(false, true)); String inputValue = "Lorem"; final String outputValue = testName.getMethodName(); String functionName = "Ipsum"; WaitFunction<String, String> waitFunction = (WaitFunction<String, String>) WaitFunction .waitFor(describe(new Function<String, String>() { @Override public String apply(String input) { return outputValue; } }).as(functionName)) .toFulfill(new Predicate<String>() { @Override public boolean apply(String input) { return predicateAnswers.pop(); } }) .within(1000L, TimeUnit.MILLISECONDS) .get(); WaitFunction<String, String> spy = Mockito.spy(waitFunction); Mockito.doThrow(InterruptedException.class).when(spy).sleep(Mockito.anyLong()); Mockito.doReturn(0L).when(spy).nowMillis(); expectedException.expect(IllegalStateException.class); expectedException.expectCause(Matchers.<Throwable>instanceOf(InterruptedException.class)); spy.apply(inputValue); } @Test public void withinMs_configures_timeout_in_milliseconds() throws Exception { String inputValue = "Lorem"; final String outputValue = testName.getMethodName(); String functionName = "Ipsum"; List<Long> timeMillis = Arrays.asList( // init: determine deadline 0L, // cycle 1: determine start time 0L, // cycle 1: determine end time 9L, // cycle 2: determine start time 9L, // cycle 2: determine end time -- timeout 11L); long timeoutMs = 10L; StoreTimeoutEvent<String, String> timeoutFunction = new StoreTimeoutEvent<>(); WaitFunction<String, String> waitFunction = (WaitFunction<String, String>) WaitFunction .waitFor(describe(new Function<String, String>() { @Override public String apply(String input) { return outputValue; } }).as(functionName)) .toFulfill(Predicates.<String>alwaysFalse()) .withinMs(timeoutMs) .onTimeout(timeoutFunction) .get(); WaitFunction<String, String> spy = Mockito.spy(waitFunction); Mockito.doNothing().when(spy).sleep(Mockito.anyLong()); Mockito.doAnswer(AdditionalAnswers.returnsElementsOf(timeMillis)).when(spy).nowMillis(); spy.apply(inputValue); assertThat("Consumed millis greater than or equal to timeout.", timeoutFunction.getLastEvent().getConsumedMs(), Matchers.greaterThanOrEqualTo(timeoutMs)); } @Test public void raised_timeout_event_contains_all_relevant_information() throws Exception { String inputValue = "Lorem"; final String outputValue = testName.getMethodName(); String functionName = "Ipsum"; long expectedConsumedMs = 11; List<Long> timeMillis = Arrays.asList( // init: determine deadline 5L, // cycle 1: determine start time 5L, // cycle 1: determine end time 5L + expectedConsumedMs); long timeoutMs = 10L; StoreTimeoutEvent<String, String> timeoutFunction = new StoreTimeoutEvent<>(); WaitFunction<String, String> waitFunction = (WaitFunction<String, String>) WaitFunction .waitFor(describe(new Function<String, String>() { @Override public String apply(String input) { return outputValue; } }).as(functionName)) .toFulfill(Predicates.<String>alwaysFalse()) .withinMs(timeoutMs) .onTimeout(timeoutFunction) .get(); WaitFunction<String, String> spy = Mockito.spy(waitFunction); Mockito.doNothing().when(spy).sleep(Mockito.anyLong()); Mockito.doAnswer(AdditionalAnswers.returnsElementsOf(timeMillis)).when(spy).nowMillis(); spy.apply(inputValue); WaitTimeoutEvent<String, String> event = timeoutFunction.getLastEvent(); String eventDescription = event.describe(); String eventToString = event.toString(); // Validate fields errorCollector.checkThat("Event contains item function got applied to.", event.getItem(), Matchers.equalTo(inputValue)); errorCollector.checkThat("Event contains WaitFunction which caused the event.", event.getSource(), Matchers.sameInstance(spy)); errorCollector.checkThat("Event contains result of last apply of function.", event.getLastResult(), Matchers.equalTo(outputValue)); errorCollector.checkThat("Event contains information on consumed milliseconds.", event.getConsumedMs(), Matchers.equalTo(expectedConsumedMs)); // Validate description errorCollector.checkThat("Event description contains item function got applied to.", eventDescription, Matchers.containsString(event.getItem())); errorCollector.checkThat("Event description contains result of last apply of function.", eventDescription, Matchers.containsString(event.getLastResult())); errorCollector.checkThat("Event description contains information on consumed milliseconds.", eventDescription, Matchers.containsString(Long.toString(event.getConsumedMs()))); // Validate toString errorCollector.checkThat("Event toString contains WaitFunction which caused the event.", eventToString, Matchers.containsString(String.valueOf(event.getSource()))); errorCollector.checkThat("Event toString contains item function got applied to.", eventToString, Matchers.containsString(event.getItem())); errorCollector.checkThat("Event toString contains result of last apply of function.", eventToString, Matchers.containsString(event.getLastResult())); errorCollector.checkThat("Event toString contains information on consumed milliseconds.", eventToString, Matchers.containsString(Long.toString(event.getConsumedMs()))); } private static class StoreTimeoutEvent<T, R> implements Function<WaitTimeoutEvent<T, R>, R> { private WaitTimeoutEvent<T, R> lastEvent; @Override public R apply(@Nullable WaitTimeoutEvent<T, R> input) { assert input != null : "null unexpected here"; lastEvent = input; return input.getLastResult(); } public WaitTimeoutEvent<T, R> getLastEvent() { return lastEvent; } } }