/*****************************************************************************
* ------------------------------------------------------------------------- *
* 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.google.mu.util;
import static com.google.common.truth.Truth.assertThat;
import static com.google.mu.util.FutureAssertions.assertCauseOf;
import static com.google.mu.util.FutureAssertions.assertCompleted;
import static com.google.mu.util.FutureAssertions.assertPending;
import static com.google.mu.util.stream.MoreStreams.iterateThrough;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static java.util.stream.Collectors.toList;
import static org.junit.Assert.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import com.google.common.testing.ClassSanityTester;
import com.google.common.testing.EqualsTester;
import com.google.common.testing.NullPointerTester;
import com.google.common.truth.IterableSubject;
import javassist.Modifier;
@RunWith(JUnit4.class)
public class MaybeTest {
@Test public void testOfNull() throws Throwable {
assertThat(Maybe.of(null).orElseThrow()).isEqualTo(null);
assertThat(Maybe.of(null).toString()).isEqualTo("null");
}
@Test public void testOrElseThrow_success() throws Throwable {
assertThat(Maybe.of("test").orElseThrow()).isEqualTo("test");
}
@Test public void testOrElseThrow_failure() throws Throwable {
MyException exception = new MyException("test");
Maybe<?, MyException> maybe = Maybe.except(exception);
MyException thrown = assertThrows(MyException.class, maybe::orElseThrow);
assertThat(thrown.getCause()).isSameAs(exception);
assertThat(thrown.getSuppressed()).isEmpty();
}
@Test public void testOrElseThrow_failureWithCause() throws Throwable {
MyException exception = new MyException("test");
Exception cause = new RuntimeException();
exception.initCause(cause);
Maybe<?, MyException> maybe = Maybe.except(exception);
MyException thrown = assertThrows(MyException.class, maybe::orElseThrow);
assertThat(thrown.getCause()).isSameAs(exception);
assertThat(thrown.getSuppressed()).isEmpty();
}
@Test public void testOrElseThrow_failureWithSuppressed() throws Throwable {
MyException exception = new MyException("test");
Exception suppressed = new RuntimeException();
exception.addSuppressed(suppressed);
Maybe<?, MyException> maybe = Maybe.except(exception);
MyException thrown = assertThrows(MyException.class, maybe::orElseThrow);
assertThat(thrown.getCause()).isSameAs(exception);
assertThat(thrown.getSuppressed()).isEmpty();
}
@Test public void testOrElseThrow_interruptedException() throws Throwable {
Maybe<?, InterruptedException> maybe = Maybe.except(new InterruptedException());
assertThat(Thread.interrupted()).isTrue();
Thread.currentThread().interrupt();
InterruptedException interrupted = assertThrows(InterruptedException.class, maybe::orElseThrow);
assertThat(interrupted.getCause()).isNull();
assertThat(Thread.interrupted()).isFalse();
}
@Test public void testOrElseThrow_exceptionCannotBeDeserialized() throws Throwable {
ExceptionWithBadSerialization exception = new ExceptionWithBadSerialization();
Maybe<?, ExceptionWithBadSerialization> maybe = Maybe.except(exception);
ExceptionWithBadSerialization thrown =
assertThrows(ExceptionWithBadSerialization.class, maybe::orElseThrow);
assertThat(thrown).isSameAs(exception);
}
@Test public void testOrElseThrow_exceptionSerializedToSubtype() throws Throwable {
ExceptionWithCustomSerialization exception = new ExceptionWithCustomSerialization();
Maybe<?, ExceptionWithCustomSerialization> maybe = Maybe.except(exception);
ExceptionWithCustomSerialization thrown =
assertThrows(ExceptionWithCustomSerialization.class, maybe::orElseThrow);
assertThat(thrown.getCause()).isSameAs(exception);
}
@Test public void testOrElseThrow_explicitException_success() throws Throwable {
assertThat(Maybe.of("test").orElseThrow(IOException::new)).isEqualTo("test");
}
@Test public void testOrElseThrow_explicitException_failure() throws Throwable {
MyException exception = new MyException("test");
Maybe<?, MyException> maybe = Maybe.except(exception);
MyException thrown = assertThrows(MyException.class, () -> maybe.orElseThrow(MyException::new));
assertSame(exception, thrown.getCause());
}
@Test public void testMap_success() {
Maybe<Integer, MyException> maybe = Maybe.of(1);
assertThat(maybe.map(Object::toString)).isEqualTo(Maybe.of("1"));
}
@Test public void testMap_failure() throws Throwable {
MyException exception = new MyException("test");
Maybe<?, MyException> maybe = Maybe.except(exception).map(Object::toString);
MyException thrown = assertThrows(MyException.class, maybe::orElseThrow);
assertThat(thrown.getCause()).isSameAs(exception);
assertThat(thrown.getSuppressed()).isEmpty();
}
@Test public void testFlatMap_success() {
Maybe<Integer, MyException> maybe = Maybe.of(1);
assertThat(maybe.flatMap(o -> Maybe.of(o.toString()))).isEqualTo(Maybe.of("1"));
}
@Test public void testFlatMap_failure() throws Throwable {
MyException exception = new MyException("test");
Maybe<?, MyException> maybe = Maybe.except(exception).flatMap(o -> Maybe.of(o.toString()));
MyException thrown = assertThrows(MyException.class, maybe::orElseThrow);
assertThat(thrown.getCause()).isSameAs(exception);
assertThat(thrown.getSuppressed()).isEmpty();
}
@Test public void testIsPresent() {
assertThat(Maybe.of(1).isPresent()).isTrue();
assertThat(Maybe.except(new Exception()).isPresent()).isFalse();
}
@Test public void testIfPresent_success() {
AtomicInteger succeeded = new AtomicInteger();
Maybe.of(100).ifPresent(i -> succeeded.set(i));
assertThat(succeeded.get()).isEqualTo(100);
}
@Test public void testIfPresent_failure() {
AtomicBoolean succeeded = new AtomicBoolean();
Maybe.except(new Exception()).ifPresent(i -> succeeded.set(true));
assertThat(succeeded.get()).isFalse();
}
@Test public void testOrElse() {
assertThat(Maybe.of("good").orElse(Throwable::getMessage)).isEqualTo("good");
assertThat(Maybe.except(new Exception("bad")).orElse(Throwable::getMessage)).isEqualTo("bad");
}
@Test public void testCatching_success() {
AtomicReference<Throwable> failed = new AtomicReference<>();
Maybe.of(100).catching(e -> {failed.set(e);});
assertThat(failed.get()).isNull();
}
@Test public void testCatching_failure() {
MyException exception = new MyException("test");
AtomicReference<Throwable> failed = new AtomicReference<>();
Maybe.except(exception).catching(e -> {failed.set(e);});
assertThat(failed.get()).isSameAs(exception);
}
@Test public void testNulls_staticMethods() {
for (Method method : Maybe.class.getMethods()) {
if (method.isSynthetic()) continue;
if (method.getName().equals("of")) continue;
if (Modifier.isStatic(method.getModifiers())) {
new NullPointerTester().testMethod(null, method);
}
}
}
@Test public void testNulls_instanceMethods() throws Exception {
new ClassSanityTester()
.forAllPublicStaticMethods(Maybe.class)
.testNulls();
}
@Test public void testEquals() {
Exception exception = new Exception();
new EqualsTester()
.addEqualityGroup(Maybe.of(1), Maybe.of(1))
.addEqualityGroup(Maybe.of(null), Maybe.of(null))
.addEqualityGroup(Maybe.of(2))
.addEqualityGroup(Maybe.except(exception), Maybe.except(exception))
.addEqualityGroup(Maybe.except(new RuntimeException()))
.testEquals();
}
@Test public void testStream_success() throws MyException {
assertStream(Stream.of("hello", "friend").map(Maybe.maybe(this::justReturn)))
.containsExactly("hello", "friend").inOrder();
}
@Test public void testStreamFlatMap_success() throws MyException {
assertStream(Stream.of("hello", "friend").flatMap(Maybe.maybeStream(this::streamOf)))
.containsExactly("hello", "friend").inOrder();
}
@Test public void testStream_exception() {
Stream<Maybe<String, MyException>> stream =
Stream.of("hello", "friend").map(Maybe.maybe(this::raise));
assertThrows(MyException.class, () -> collect(stream));
}
@Test public void testStreamFlatMap_exception() {
Stream<Maybe<String, MyException>> stream =
Stream.of("hello", "friend").flatMap(Maybe.maybeStream(this::raiseForStream));
assertThrows(MyException.class, () -> collect(stream));
}
@Test public void testStream_interrupted() {
Stream<Maybe<String, InterruptedException>> stream =
Stream.of(1, 2).map(Maybe.maybe(x -> hibernate()));
Thread.currentThread().interrupt();
try {
assertThrows(InterruptedException.class, () -> collect(stream));
} finally {
assertThat(Thread.interrupted()).isFalse();
}
}
@Test public void testStream_uncheckedExceptionNotCaptured() {
Stream<String> stream = Stream.of("hello", "friend")
.map(Maybe.maybe(this::raiseUnchecked))
.flatMap(m -> m.catching(e -> {}));
assertThrows(RuntimeException.class, () -> stream.collect(toList()));
}
@Test public void testStream_swallowException() {
assertThat(Stream.of("hello", "friend")
.map(Maybe.maybe(this::raise))
.flatMap(m -> m.catching(e -> {}))
.collect(toList()))
.isEmpty();
}
@Test public void testStream_generateSuccess() {
assertThat(Stream.generate(() -> Maybe.maybe(() -> justReturn("good"))).findFirst().get())
.isEqualTo(Maybe.of("good"));
}
@Test public void testStream_generateFailure() {
Maybe<String, MyException> maybe =
Stream.generate(() -> Maybe.maybe(() -> raise("bad"))).findFirst().get();
assertThat(assertThrows(MyException.class, maybe::orElseThrow).getMessage()).isEqualTo("bad");
}
@Test public void testFilterByValue_successValueFiltered() throws MyException {
assertStream(Stream.of("hello", "friend")
.map(Maybe.maybe(this::justReturn))
.filter(Maybe.byValue("hello"::equals)))
.containsExactly("hello");
}
@Test public void testFilterByValue_failuresNotFiltered() {
List<Maybe<String, MyException>> maybes = Stream.of("hello", "friend")
.map(Maybe.maybe(this::raise))
.filter(Maybe.byValue(s -> false))
.collect(toList());
assertThat(maybes).hasSize(2);
assertThat(assertThrows(MyException.class, () -> maybes.get(0).orElseThrow()).getMessage())
.isEqualTo("hello");
assertThat(assertThrows(MyException.class, () -> maybes.get(1).orElseThrow()).getMessage())
.isEqualTo("friend");
}
@Test public void wrapFuture_futureIsSuccess() throws Exception {
CompletionStage<Maybe<String, Exception>> stage =
Maybe.catchException(Exception.class, completedFuture("good"));
assertCompleted(stage).isEqualTo(Maybe.of("good"));
}
@Test public void wrapFuture_futureIsSuccessNull() throws Exception {
CompletionStage<Maybe<String, Exception>> stage =
Maybe.catchException(Exception.class, completedFuture(null));
assertThat(completedFuture(null).isDone()).isTrue();
assertCompleted(stage).isEqualTo(Maybe.of(null));
}
@Test public void wrapFuture_futureIsExpectedFailure() throws Exception {
MyException exception = new MyException("test");
CompletionStage<Maybe<String, MyException>> stage =
Maybe.catchException(MyException.class, exceptionally(exception));
assertCompleted(stage).isEqualTo(Maybe.except(exception));
}
@Test public void wrapFuture_futureIsExpectedFailureNestedInExecutionException()
throws Exception {
MyUncheckedException exception = new MyUncheckedException("test");
CompletionStage<Maybe<String, MyUncheckedException>> stage =
Maybe.catchException(MyUncheckedException.class, executionExceptionally(exception));
assertCompleted(stage).isEqualTo(Maybe.except(exception));
}
@Test public void wrapFuture_futureIsUnexpectedFailure() throws Exception {
RuntimeException exception = new RuntimeException("test");
CompletionStage<Maybe<String, MyException>> stage =
Maybe.catchException(MyException.class, exceptionally(exception));
assertCauseOf(ExecutionException.class, stage).isSameAs(exception);
}
@Test public void wrapFuture_futureIsCancelledWithInterruption() throws Exception {
CompletionStage<Maybe<String, MyException>> stage =
Maybe.catchException(MyException.class, cancelled(true));
assertCauseOf(CancellationException.class, stage);
}
@Test public void wrapFuture_futureIsCancelledWithNoInterruption() throws Exception {
CompletionStage<Maybe<String, MyException>> stage =
Maybe.catchException(MyException.class, cancelled(false));
assertCauseOf(CancellationException.class, stage);
}
@Test public void wrapFuture_futureIsUnexpectedCheckedException_idempotence() throws Exception {
MyException exception = new MyException("test");
CompletionStage<?> stage =
Maybe.catchException(IOException.class, exceptionally(exception));
stage = Maybe.catchException(IOException.class, stage);
stage = Maybe.catchException(MyUncheckedException.class, stage);
assertCauseOf(ExecutionException.class, stage).isSameAs(exception);
}
@Test public void wrapFuture_futureIsUnexpectedUncheckedException_idempotence() throws Exception {
RuntimeException exception = new RuntimeException("test");
CompletionStage<?> stage =
Maybe.catchException(IOException.class, exceptionally(exception));
stage = Maybe.catchException(IOException.class, stage);
stage = Maybe.catchException(MyException.class, stage);
stage = Maybe.catchException(Error.class, stage);
assertCauseOf(ExecutionException.class, stage).isSameAs(exception);
}
@Test public void wrapFuture_futureIsUnexpectedError_idempotence() throws Exception {
Error error = new Error("test");
CompletionStage<?> stage =
Maybe.catchException(IOException.class, exceptionally(error));
stage = Maybe.catchException(IOException.class, stage);
stage = Maybe.catchException(MyException.class, stage);
stage = Maybe.catchException(MyUncheckedException.class, stage);
assertCauseOf(ExecutionException.class, stage).isSameAs(error);
}
@Test public void wrapFuture_futureIsUnexpectedFailure_notApplied() throws Exception {
RuntimeException exception = new RuntimeException("test");
CompletionStage<?> stage = exceptionally(exception);
assertCauseOf(ExecutionException.class, stage).isSameAs(exception);
}
@Test public void wrapFuture_futureBecomesSuccess() throws Exception {
CompletableFuture<String> future = new CompletableFuture<>();
CompletionStage<Maybe<String, Exception>> stage = Maybe.catchException(Exception.class, future);
assertPending(stage);
future.complete("good");
assertCompleted(stage).isEqualTo(Maybe.of("good"));
}
@Test public void wrapFuture_futureBecomesExpectedFailure() throws Exception {
CompletableFuture<String> future = new CompletableFuture<>();
CompletionStage<Maybe<String, MyException>> stage =
Maybe.catchException(MyException.class, future);
assertPending(stage);
MyException exception = new MyException("test");
future.completeExceptionally(exception);
assertCompleted(stage).isEqualTo(Maybe.except(exception));
}
@Test public void wrapFuture_transparentToHandle() throws Exception {
assertCompleted(naiveExceptionHandlingCode(exceptionalUserCode())).isNull();
assertCompleted(naiveExceptionHandlingCode(
Maybe.catchException(MyUncheckedException.class, exceptionalUserCode())))
.isNull();
}
@Test public void wrapFuture_transparentToExceptionally() throws Exception {
assertCompleted(naiveExceptionallyCode(exceptionalUserCode())).isNull();
assertCompleted(naiveExceptionallyCode(
Maybe.catchException(MyUncheckedException.class, exceptionalUserCode())))
.isNull();
}
private static CompletionStage<String> exceptionalUserCode() {
CompletableFuture<String> future = new CompletableFuture<>();
MyException exception = new MyException("test");
future.completeExceptionally(exception);
return future;
}
private static <T> CompletionStage<T> naiveExceptionHandlingCode(
CompletionStage<T> stage) {
return stage.handle((v, e) -> {
assertThat(e).isInstanceOf(MyException.class);
return null;
});
}
private static <T> CompletionStage<T> naiveExceptionallyCode(
CompletionStage<T> stage) {
return stage.exceptionally(e -> {
assertThat(e).isInstanceOf(MyException.class);
return null;
});
}
@Test public void testCompletionStage_handle_wraps() throws Exception {
CompletableFuture<String> future = new CompletableFuture<>();
MyException exception = new MyException("test");
future.completeExceptionally(exception);
CompletionStage<String> stage = future.handle((v, e) -> {
throw new CompletionException(e);
});
assertCauseOf(ExecutionException.class, stage).isSameAs(exception);
}
@Test public void testCompletionStage_exceptionally_wraps() throws Exception {
CompletableFuture<String> future = new CompletableFuture<>();
MyException exception = new MyException("test");
future.completeExceptionally(exception);
CompletionStage<String> stage = future.exceptionally(e -> {
throw new CompletionException(e);
});
assertCauseOf(ExecutionException.class, stage).isSameAs(exception);
}
@Test public void wrapFuture_futureBecomesUnexpectedFailure() throws Exception {
CompletableFuture<String> future = new CompletableFuture<>();
CompletionStage<Maybe<String, MyException>> stage =
Maybe.catchException(MyException.class, future);
assertPending(stage);
RuntimeException exception = new RuntimeException("test");
future.completeExceptionally(exception);
assertCauseOf(ExecutionException.class, stage).isSameAs(exception);
}
@Test public void testExecutionExceptionally() {
RuntimeException exception = new RuntimeException("test");
assertCauseOf(ExecutionException.class, executionExceptionally(exception))
.isSameAs(exception);
}
private static <T> CompletionStage<T> exceptionally(Throwable e) {
CompletableFuture<T> future = new CompletableFuture<>();
future.completeExceptionally(e);
return future;
}
private static <T> CompletionStage<T> cancelled(boolean mayInterruptIfRunning) {
CompletableFuture<T> future = new CompletableFuture<>();
future.cancel(mayInterruptIfRunning);
return future;
}
private static <T> CompletionStage<T> executionExceptionally(RuntimeException e) {
return completedFuture((T) null).whenComplete((v, x) -> {throw e;});
}
private String raise(String s) throws MyException {
throw new MyException(s);
}
private Stream<String> raiseForStream(String s) throws MyException {
throw new MyException(s);
}
@SuppressWarnings("unused") // Signature needed for Maybe.wrap()
private String raiseUnchecked(String s) throws MyException {
throw new RuntimeException(s);
}
@SuppressWarnings("unused") // Signature needed for Maybe.wrap()
private String justReturn(String s) throws MyException {
return s;
}
@SuppressWarnings("unused") // Signature needed for Maybe.wrap()
private Stream<String> streamOf(String s) throws MyException {
return Stream.of(s);
}
private static <T, E extends Throwable> IterableSubject assertStream(
Stream<Maybe<T, E>> stream) throws E {
return assertThat(collect(stream));
}
private static <T, E extends Throwable> List<T> collect(Stream<Maybe<T, E>> stream) throws E {
List<T> list = new ArrayList<>();
iterateThrough(stream, m -> list.add(m.orElseThrow()));
return list;
}
private static String hibernate() throws InterruptedException {
new CountDownLatch(1).await();
throw new AssertionError("can't reach here");
}
@SuppressWarnings("serial")
private static class MyException extends Exception {
MyException(String message) {
super(message);
}
MyException(Throwable cause) {
super(cause);
}
}
@SuppressWarnings("serial")
private static class MyUncheckedException extends RuntimeException {
MyUncheckedException(String message) {
super(message);
}
}
@SuppressWarnings("serial")
private static class ExceptionWithBadSerialization extends Exception {
private Object writeReplace() {
return new RuntimeException();
}
}
@SuppressWarnings("serial")
private static class ExceptionWithCustomSerialization extends Exception {
private Object writeReplace() {
return new ExceptionWithCustomSerialization() {};
}
}
}