/*
* 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.concurrent.OnWaitFunctionSpy.spyOnWaitFunction;
import static com.github.mmichaelis.hamcrest.nextdeed.reflect.ClassModifierMatcher.classModifierContains;
import static com.github.mmichaelis.hamcrest.nextdeed.reflect.InstantiableViaDefaultConstructor.isInstantiableViaDefaultConstructor;
import static com.github.mmichaelis.hamcrest.nextdeed.reflect.MemberModifierMatcher.memberModifierContains;
import static java.lang.String.valueOf;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.anyLong;
import static org.mockito.internal.verification.VerificationModeFactory.times;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.github.mmichaelis.hamcrest.nextdeed.glue.Consumer;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.jetbrains.annotations.NotNull;
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.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* Tests {@link Probe} or more specifically
* {@link ProbeBuilder}. The main focus
* is to test that the given arguments are correctly handed over to the wait function.
*
* @since 1.0.0
*/
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
@RunWith(Parameterized.class)
public class ProbeTest {
@NotNull
private final ProbeFacadeMode mode;
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Rule
public ErrorCollector errorCollector = new ErrorCollector();
@Rule
public TestName testName = new TestName();
public ProbeTest(@NotNull ProbeFacadeMode mode) {
this.mode = mode;
}
@Parameters(name = "{index}: Mode {0}")
public static Collection<Object[]> data() {
return asList(new Object[][]{
{ProbeFacadeMode.ASSERT},
{ProbeFacadeMode.ASSUME},
{ProbeFacadeMode.REQUIRE},
});
}
@Test
public void throw_exception_on_failure_no_message() throws Exception {
List<Long> usedTimeMillis = Collections.singletonList(1000L);
SystemUnderTest_SUT systemUnderTest = new SystemUnderTest_SUT();
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withInitialDelayMs(0L)
.withinMs(0L);
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("Exception should have been thrown.", result, notNullValue());
assertThat("All required information should be contained in exception message.",
result.getMessage(),
allOf(not(containsString("null")),
containsString(valueOf(SystemState.RUNNING)),
containsString(valueOf(SystemState.STOPPED))));
}
@Test
public void inform_timeout_handlers_on_failure() throws Exception {
List<Long> usedTimeMillis = Collections.singletonList(1000L);
SystemUnderTest_SUT systemUnderTest = new SystemUnderTest_SUT();
WaitTimeoutEventConsumer eventConsumer1 = new WaitTimeoutEventConsumer();
WaitTimeoutEventConsumer eventConsumer2 = new WaitTimeoutEventConsumer();
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withInitialDelayMs(0L)
.onTimeout(eventConsumer1)
.onTimeout(eventConsumer2)
.withinMs(0L);
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("Consumer 1 should have been informed.", eventConsumer1.getEvent(), notNullValue());
assertThat("Consumer 2 should have been informed.", eventConsumer2.getEvent(), notNullValue());
assertThat("Event should contain last result before failure.",
eventConsumer1.getEvent().getLastResult(), is(SystemState.STOPPED));
assertThat("Exception should have been thrown.", result, notNullValue());
assertThat("All required information should be contained in exception message.",
result.getMessage(),
allOf(not(containsString("null")),
containsString(valueOf(SystemState.RUNNING)),
containsString(valueOf(SystemState.STOPPED))));
}
@Test
public void throw_exception_on_failure_with_message() throws Exception {
List<Long> usedTimeMillis = Collections.singletonList(1000L);
SystemUnderTest_SUT systemUnderTest = new SystemUnderTest_SUT();
String message = testName.getMethodName();
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withInitialDelayMs(0L)
.withinMs(0L);
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, message, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("Exception should have been thrown.", result, notNullValue());
assertThat("All required information should be contained in exception message.",
result.getMessage(),
allOf(containsString(message),
containsString(valueOf(SystemState.RUNNING)),
containsString(valueOf(SystemState.STOPPED))));
}
@Test
public void pass_on_second_try_with_message() throws Exception {
List<Long> usedTimeMillis = asList(1000L, 2000L);
SystemUnderTest_SUT
systemUnderTest = new SystemUnderTest_SUT(SystemState.STOPPED, SystemState.RUNNING);
String message = testName.getMethodName();
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withinMs(1000L);
OnWaitFunctionSpy functionSpy =
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, message, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("No exception should have been thrown.", result, nullValue());
WaitFunction<SystemUnderTest_SUT, SystemState> spy = functionSpy.getWaitFunction();
Mockito.verify(spy, times(1)).sleep(anyLong());
}
@Test
public void pass_on_second_try_without_message() throws Exception {
List<Long> usedTimeMillis = asList(1000L, 2000L);
SystemUnderTest_SUT
systemUnderTest = new SystemUnderTest_SUT(SystemState.STOPPED, SystemState.RUNNING);
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withinMs(1000L);
OnWaitFunctionSpy functionSpy =
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("No exception should have been thrown.", result, nullValue());
WaitFunction<SystemUnderTest_SUT, SystemState> spy = functionSpy.getWaitFunction();
Mockito.verify(spy, times(1)).sleep(anyLong());
}
@Test
public void pass_on_eventual_match() throws Exception {
List<Long> usedTimeMillis = asList(1000L, 2000L);
SystemUnderTest_SUT systemUnderTest =
new SystemUnderTest_SUT(SystemState.STOPPED, SystemState.RUNNING, SystemState.RUNNING);
String message = testName.getMethodName();
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withinMs(1000L);
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, message, new GetSystemState(), new SameStateTwice());
assertThat("No exception should have been thrown.", result, nullValue());
}
@Test
public void deceleration_factor_is_forwarded_to_wait_function() throws Exception {
List<Long> usedTimeMillis = asList(3L, 5L, 7L);
SystemUnderTest_SUT
systemUnderTest =
new SystemUnderTest_SUT(SystemState.STOPPED, SystemState.STOPPED, SystemState.RUNNING);
String message = testName.getMethodName();
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withInitialDelay(1, TimeUnit.MILLISECONDS)
.deceleratePollingBy(2.0d)
.and()
.within(1L, TimeUnit.SECONDS);
OnWaitFunctionSpy functionSpy =
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, message, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("No exception should have been thrown.", result, nullValue());
WaitFunction<SystemUnderTest_SUT, SystemState> spy = functionSpy.getWaitFunction();
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(),
equalTo(asList(3L, 6L)));
}
@Test
public void initial_delay_2arg_is_forwarded_to_wait_function() throws Exception {
List<Long> usedTimeMillis = asList(3L, 5L, 200L, 7L);
SystemUnderTest_SUT
systemUnderTest =
new SystemUnderTest_SUT(SystemState.STOPPED, SystemState.STOPPED, SystemState.STARTING,
SystemState.RUNNING);
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withInitialDelay(100L, TimeUnit.MILLISECONDS)
.deceleratePollingBy(1.5d)
.and()
.within(1L, TimeUnit.SECONDS);
OnWaitFunctionSpy
functionSpy =
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("No exception should have been thrown.", result, nullValue());
WaitFunction<SystemUnderTest_SUT, SystemState> spy = functionSpy.getWaitFunction();
ArgumentCaptor<Long> argument = ArgumentCaptor.forClass(Long.class);
InOrder inOrder = Mockito.inOrder(spy);
inOrder.verify(spy).sleep(argument.capture());
inOrder.verify(spy).sleep(argument.capture());
inOrder.verify(spy).sleep(argument.capture());
assertThat("Initial delay is respected and eventually overridden when system becomes slow.",
argument.getAllValues(),
equalTo(asList(100L, 150L, 225L)));
}
@Test
public void initial_delay_1arg_is_forwarded_to_wait_function() throws Exception {
long initialDelayMs = 100L;
double decelerationFactor = 1.5d;
long slowSystemMs = 300L;
List<Long> usedTimeMillis = asList(3L, 5L, slowSystemMs, 7L);
SystemUnderTest_SUT
systemUnderTest =
new SystemUnderTest_SUT(SystemState.STOPPED, SystemState.STOPPED, SystemState.STARTING,
SystemState.RUNNING);
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withInitialDelayMs(initialDelayMs)
.deceleratePollingBy(decelerationFactor)
.and()
.within(1L, TimeUnit.SECONDS);
OnWaitFunctionSpy functionSpy =
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("No exception should have been thrown.", result, nullValue());
WaitFunction<SystemUnderTest_SUT, SystemState> spy = functionSpy.getWaitFunction();
ArgumentCaptor<Long> argument = ArgumentCaptor.forClass(Long.class);
InOrder inOrder = Mockito.inOrder(spy);
inOrder.verify(spy).sleep(argument.capture());
inOrder.verify(spy).sleep(argument.capture());
inOrder.verify(spy).sleep(argument.capture());
assertThat("Initial delay is respected and eventually overridden when system becomes slow.",
argument.getAllValues(),
equalTo(asList(
initialDelayMs,
Math.round(initialDelayMs * decelerationFactor),
slowSystemMs)));
}
@Test
public void grace_period_1arg_is_respected() throws Exception {
long initialDelayMs = 0L;
long gracePeriodMs = 50L;
double decelerationFactor = 1d;
List<Long> usedTimeMillis = asList(100L, 100L);
SystemUnderTest_SUT
systemUnderTest = new SystemUnderTest_SUT(SystemState.STOPPED, SystemState.RUNNING);
String message = testName.getMethodName();
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withInitialDelayMs(initialDelayMs)
.withFinalGracePeriodMs(gracePeriodMs)
.deceleratePollingBy(decelerationFactor)
.and()
.within(140L, TimeUnit.MILLISECONDS);
OnWaitFunctionSpy
functionSpy =
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, message, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("No exception should have been thrown.", result, nullValue());
WaitFunction<SystemUnderTest_SUT, SystemState> spy = functionSpy.getWaitFunction();
ArgumentCaptor<Long> argument = ArgumentCaptor.forClass(Long.class);
InOrder inOrder = Mockito.inOrder(spy);
inOrder.verify(spy).sleep(argument.capture());
assertThat(
"Without grace period only 40 ms would remain after first poll. With grace we should get 90 ms.",
argument.getAllValues(),
equalTo(Collections.singletonList(90L)));
}
@Test
public void grace_period_2arg_is_respected() throws Exception {
long initialDelayMs = 0L;
long gracePeriodMs = 50L;
double decelerationFactor = 1d;
List<Long> usedTimeMillis = asList(100L, 100L);
SystemUnderTest_SUT
systemUnderTest = new SystemUnderTest_SUT(SystemState.STOPPED, SystemState.RUNNING);
String message = testName.getMethodName();
ProbeBuilder<SystemUnderTest_SUT, SystemState> configuredProbe =
Probe.<SystemUnderTest_SUT, SystemState>probing(systemUnderTest)
.withInitialDelayMs(initialDelayMs)
.withFinalGracePeriod(gracePeriodMs, TimeUnit.MILLISECONDS)
.deceleratePollingBy(decelerationFactor)
.and()
.within(140L, TimeUnit.MILLISECONDS);
OnWaitFunctionSpy functionSpy =
spyOnWaitFunction((ProbeBuilderImpl<SystemUnderTest_SUT, SystemState>) configuredProbe,
usedTimeMillis);
Throwable result =
new ProbeFacade<>(configuredProbe)
.run(mode, message, new GetSystemState(), equalTo(SystemState.RUNNING));
assertThat("No exception should have been thrown.", result, nullValue());
WaitFunction<SystemUnderTest_SUT, SystemState> spy = functionSpy.getWaitFunction();
ArgumentCaptor<Long> argument = ArgumentCaptor.forClass(Long.class);
InOrder inOrder = Mockito.inOrder(spy);
inOrder.verify(spy).sleep(argument.capture());
assertThat(
"Without grace period only 40 ms would remain after first poll. With grace we should get 90 ms.",
argument.getAllValues(),
equalTo(Collections.singletonList(90L)));
}
@Test
public void probeBuilder_has_toString() throws Exception {
Random random = new Random(0);
long initialDelayMs = random.nextInt(1000);
double decelerationFactor = Math.abs(random.nextDouble() - 1d) + 1;
long gracePeriodMs = random.nextInt(1000);
long timeoutMs = random.nextInt(1000);
ProbeBuilder<String, String> configuredProbe =
Probe.<String, String>probing(testName.getMethodName())
.withInitialDelayMs(initialDelayMs)
.deceleratePollingBy(decelerationFactor)
.withFinalGracePeriodMs(gracePeriodMs)
.withinMs(timeoutMs);
assertThat("toString shows all configured values.",
configuredProbe,
hasToString(allOf(
containsString(configuredProbe.getClass().getSimpleName()),
containsString(testName.getMethodName()),
containsString(valueOf(initialDelayMs)),
containsString(valueOf(decelerationFactor)),
containsString(valueOf(gracePeriodMs)),
containsString(valueOf(timeoutMs))
)
)
);
}
@Test
public void probeIsUtilityClass() throws Exception {
errorCollector.checkThat("Class must be final.",
Probe.class,
classModifierContains(Modifier.FINAL));
errorCollector.checkThat("Any constructors must be private.",
asList(Probe.class.getDeclaredConstructors()),
everyItem(memberModifierContains(Modifier.PRIVATE)));
assertThat("Default constructor must exist.",
Probe.class,
isInstantiableViaDefaultConstructor());
}
private enum ProbeFacadeMode {
ASSERT,
ASSUME,
REQUIRE
}
private static final class ProbeFacade<T, R>
implements ProbeAssert<T, R>, ProbeAssume<T, R>, ProbeRequire<T, R> {
@NotNull
private final ProbeBuilder<T, R> delegateProbeBuilder;
private ProbeFacade(@NotNull ProbeBuilder<T, R> delegateProbeBuilder) {
this.delegateProbeBuilder = delegateProbeBuilder;
}
@Override
public void assertThat(@NotNull Function<T, R> actualFunction,
@NotNull Matcher<? super R> matcher) {
delegateProbeBuilder.assertThat(actualFunction, matcher);
}
@Override
public void assertThat(@Nullable String reason, @NotNull Function<T, R> actualFunction,
@NotNull Matcher<? super R> matcher) {
delegateProbeBuilder.assertThat(reason, actualFunction, matcher);
}
@Override
public void assumeThat(@NotNull Function<T, R> actualFunction,
@NotNull Matcher<? super R> matcher) {
delegateProbeBuilder.assumeThat(actualFunction, matcher);
}
@Override
public void assumeThat(@Nullable String reason, @NotNull Function<T, R> actualFunction,
@NotNull Matcher<? super R> matcher) {
delegateProbeBuilder.assumeThat(reason, actualFunction, matcher);
}
@Override
public void requireThat(@NotNull Function<T, R> actualFunction,
@NotNull Matcher<? super R> matcher) {
delegateProbeBuilder.requireThat(actualFunction, matcher);
}
@Override
public void requireThat(@Nullable String reason, @NotNull Function<T, R> actualFunction,
@NotNull Matcher<? super R> matcher) {
delegateProbeBuilder.requireThat(reason, actualFunction, matcher);
}
@Nullable
public Throwable run(@NotNull ProbeFacadeMode mode,
@NotNull Function<T, R> actualFunction,
@NotNull Matcher<? super R> matcher) {
try {
switch (mode) {
case ASSERT:
assertThat(actualFunction, matcher);
break;
case ASSUME:
assumeThat(actualFunction, matcher);
break;
case REQUIRE:
requireThat(actualFunction, matcher);
break;
}
} catch (Throwable e) {
return e;
}
return null;
}
@Nullable
public Throwable run(@NotNull ProbeFacadeMode mode,
@Nullable String reason,
@NotNull Function<T, R> actualFunction,
@NotNull Matcher<? super R> matcher) {
try {
switch (mode) {
case ASSERT:
assertThat(reason, actualFunction, matcher);
break;
case ASSUME:
assumeThat(reason, actualFunction, matcher);
break;
case REQUIRE:
requireThat(reason, actualFunction, matcher);
break;
}
} catch (Throwable e) {
return e;
}
return null;
}
}
private static class SameStateTwice extends TypeSafeMatcher<SystemState> {
private AtomicReference<SystemState> previousState = new AtomicReference<>();
@Override
public void describeTo(Description description) {
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("hash", Integer.toHexString(System.identityHashCode(this)))
.add("super", super.toString())
.add("previousState", previousState)
.toString();
}
@Override
protected boolean matchesSafely(SystemState item) {
return item == previousState.getAndSet(item);
}
}
private static class WaitTimeoutEventConsumer
implements Consumer<WaitTimeoutEvent<SystemUnderTest_SUT, SystemState>> {
private WaitTimeoutEvent<SystemUnderTest_SUT, SystemState> event;
@Override
public void accept(WaitTimeoutEvent<SystemUnderTest_SUT, SystemState> event) {
this.event = event;
}
public WaitTimeoutEvent<SystemUnderTest_SUT, SystemState> getEvent() {
return event;
}
}
}