1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.internal.util; 17 18 import static com.google.common.truth.Truth.assertThat; 19 import static org.junit.Assert.assertThrows; 20 21 import com.google.android.libraries.mobiledatadownload.internal.util.FuturesUtil.SequentialFutureChain; 22 import com.google.common.base.Function; 23 import com.google.common.util.concurrent.Futures; 24 import com.google.common.util.concurrent.ListenableFuture; 25 import com.google.common.util.concurrent.MoreExecutors; 26 import java.io.IOException; 27 import java.util.concurrent.ExecutionException; 28 import java.util.concurrent.Executor; 29 import java.util.concurrent.Executors; 30 import org.junit.Before; 31 import org.junit.Test; 32 import org.junit.runner.RunWith; 33 import org.robolectric.RobolectricTestRunner; 34 35 @RunWith(RobolectricTestRunner.class) 36 public final class FuturesUtilTest { 37 38 private static final Executor SEQUENTIAL_EXECUTOR = 39 MoreExecutors.newSequentialExecutor(Executors.newCachedThreadPool()); 40 41 private FuturesUtil futuresUtil; 42 43 @Before setUp()44 public void setUp() { 45 futuresUtil = new FuturesUtil(SEQUENTIAL_EXECUTOR); 46 } 47 48 @Test chain_preservesOrder()49 public void chain_preservesOrder() throws ExecutionException, InterruptedException { 50 SequentialFutureChain<String> chain = futuresUtil.newSequentialChain(""); 51 for (int i = 0; i < 10; i++) { 52 // Variables captured in lambdas must be effectively final. 53 int index = i; 54 chain.chainAsync(str -> Futures.immediateFuture(str + index)); 55 } 56 String result = chain.start().get(); 57 assertThat(result).isEqualTo("0123456789"); 58 } 59 60 @Test chain_mixedOperationTypes_preservesOrder()61 public void chain_mixedOperationTypes_preservesOrder() 62 throws ExecutionException, InterruptedException { 63 String result = 64 futuresUtil 65 .newSequentialChain("") 66 .chainAsync(str -> Futures.immediateFuture(str + 0)) 67 .chain(str -> str + 1) 68 .chainAsync(str -> Futures.immediateFuture(str + 2)) 69 .chain(str -> str + 3) 70 .chain(str -> str + 4) 71 .chainAsync(str -> Futures.immediateFuture(str + 5)) 72 .start() 73 .get(); 74 assertThat(result).isEqualTo("012345"); 75 } 76 77 @Test chain_preservesOrder_sideEffects()78 public void chain_preservesOrder_sideEffects() throws ExecutionException, InterruptedException { 79 StringBuilder sb = new StringBuilder(""); 80 SequentialFutureChain<Void> chain = futuresUtil.newSequentialChain(); 81 for (int i = 0; i < 10; i++) { 82 chain.chainAsync(appendIntAsync(sb, i)); 83 } 84 chain.start().get(); 85 assertThat(sb.toString()).isEqualTo("0123456789"); 86 } 87 88 @Test chain_mixedOperationTypes_preservesOrder_sideEffects()89 public void chain_mixedOperationTypes_preservesOrder_sideEffects() 90 throws ExecutionException, InterruptedException { 91 StringBuilder sb = new StringBuilder(""); 92 futuresUtil 93 .newSequentialChain() 94 .chain(appendIntDirect(sb, 0)) 95 .chainAsync(appendIntAsync(sb, 1)) 96 .chain(appendIntDirect(sb, 2)) 97 .chainAsync(appendIntAsync(sb, 3)) 98 .chainAsync(appendIntAsync(sb, 4)) 99 .chain(appendIntDirect(sb, 5)) 100 .start() 101 .get(); 102 assertThat(sb.toString()).isEqualTo("012345"); 103 } 104 105 @Test chain_noOp_preservesInitialState()106 public void chain_noOp_preservesInitialState() throws ExecutionException, InterruptedException { 107 StringBuilder sb = new StringBuilder("Initial state."); 108 SequentialFutureChain<Void> chain = futuresUtil.newSequentialChain(); 109 for (int i = 0; i < 0; i++) { 110 chain.chainAsync(appendIntAsync(sb, i)); 111 } 112 chain.start().get(); 113 assertThat(sb.toString()).isEqualTo("Initial state."); 114 } 115 116 @Test chain_propagatesFailure()117 public void chain_propagatesFailure() { 118 StringBuilder sb = new StringBuilder(""); 119 SequentialFutureChain<Void> chain = futuresUtil.newSequentialChain(); 120 chain 121 .chainAsync(appendIntAsync(sb, 0)) 122 .chainAsync(appendIntAsync(sb, 1)) 123 .chainAsync(voidArg -> Futures.immediateFailedFuture(new IOException("The error message."))) 124 .chainAsync(appendIntAsync(sb, 2)) 125 .chainAsync(appendIntAsync(sb, 3)); 126 ExecutionException ex = assertThrows(ExecutionException.class, chain.start()::get); 127 assertThat(ex).hasCauseThat().isInstanceOf(IOException.class); 128 assertThat(ex).hasMessageThat().contains("The error message."); 129 } 130 131 @Test chain_mixedOperationTypes_propagatesFailure()132 public void chain_mixedOperationTypes_propagatesFailure() { 133 StringBuilder sb = new StringBuilder(""); 134 SequentialFutureChain<Void> chain = futuresUtil.newSequentialChain(); 135 chain 136 .chainAsync(appendIntAsync(sb, 0)) 137 .chain(appendIntDirect(sb, 1)) 138 .chain( 139 voidArg -> { 140 throw new RuntimeException("The error message."); 141 }) 142 .chain(appendIntDirect(sb, 2)) 143 .chainAsync(appendIntAsync(sb, 3)); 144 ExecutionException ex = assertThrows(ExecutionException.class, chain.start()::get); 145 assertThat(ex).hasCauseThat().isInstanceOf(RuntimeException.class); 146 assertThat(ex).hasMessageThat().contains("The error message."); 147 } 148 appendIntDirect(StringBuilder sb, int i)149 private static Function<Void, Void> appendIntDirect(StringBuilder sb, int i) { 150 return voidArg -> { 151 sb.append(i); 152 return null; 153 }; 154 } 155 appendIntAsync(StringBuilder sb, int i)156 private static Function<Void, ListenableFuture<Void>> appendIntAsync(StringBuilder sb, int i) { 157 return voidArg -> { 158 sb.append(i); 159 return Futures.immediateFuture(null); 160 }; 161 } 162 } 163