1 /* <lambda>null2 * Copyright (C) 2022 The Dagger Authors. 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 17 package dagger.functional.kotlinsrc.cycle 18 19 import com.google.common.truth.Truth.assertThat 20 import com.google.common.util.concurrent.SettableFuture 21 import com.google.common.util.concurrent.Uninterruptibles 22 import dagger.Component 23 import dagger.Module 24 import dagger.Provides 25 import java.lang.Exception 26 import java.lang.RuntimeException 27 import java.lang.Thread.State.BLOCKED 28 import java.lang.Thread.State.WAITING 29 import java.util.ArrayList 30 import java.util.Collections 31 import java.util.concurrent.CountDownLatch 32 import java.util.concurrent.ExecutionException 33 import java.util.concurrent.atomic.AtomicInteger 34 import java.util.function.Consumer 35 import javax.inject.Provider 36 import javax.inject.Qualifier 37 import javax.inject.Singleton 38 import org.junit.Assert.fail 39 import org.junit.Test 40 import org.junit.runner.RunWith 41 import org.junit.runners.JUnit4 42 43 @RunWith(JUnit4::class) 44 class DoubleCheckCycleTest { 45 // TODO(b/77916397): Migrate remaining tests in DoubleCheckTest to functional tests in this class. 46 /** A qualifier for a reentrant scoped binding. */ 47 @Retention(AnnotationRetention.RUNTIME) 48 @Qualifier 49 internal annotation class Reentrant 50 51 /** A module to be overridden in each test. */ 52 @Module 53 internal open class OverrideModule { 54 @Provides 55 @Singleton 56 open fun provideObject(): Any { 57 throw IllegalStateException("This method should be overridden in tests") 58 } 59 60 @Provides 61 @Singleton 62 @Reentrant 63 open fun provideReentrantObject(@Reentrant provider: Provider<Any>): Any { 64 throw IllegalStateException("This method should be overridden in tests") 65 } 66 } 67 68 @Singleton 69 @Component(modules = [OverrideModule::class]) 70 internal interface TestComponent { 71 fun getObject(): Any 72 73 @Reentrant 74 fun getReentrantObject(): Any 75 } 76 77 @Test 78 fun testNonReentrant() { 79 val callCount = AtomicInteger(0) 80 81 // Provides a non-reentrant binding. The provides method should only be called once. 82 val component = 83 DaggerDoubleCheckCycleTest_TestComponent.builder() 84 .overrideModule( 85 object : OverrideModule() { 86 override fun provideObject(): Any { 87 callCount.getAndIncrement() 88 return Any() 89 } 90 } 91 ) 92 .build() 93 assertThat(callCount.get()).isEqualTo(0) 94 val first = component.getObject() 95 assertThat(callCount.get()).isEqualTo(1) 96 val second = component.getObject() 97 assertThat(callCount.get()).isEqualTo(1) 98 assertThat(first).isSameInstanceAs(second) 99 } 100 101 @Test 102 fun testReentrant() { 103 val callCount = AtomicInteger(0) 104 105 // Provides a reentrant binding. Even though it's scoped, the provides method is called twice. 106 // In this case, we allow it since the same instance is returned on the second call. 107 val component = 108 DaggerDoubleCheckCycleTest_TestComponent.builder() 109 .overrideModule( 110 object : OverrideModule() { 111 override fun provideReentrantObject(provider: Provider<Any>): Any { 112 return if (callCount.incrementAndGet() == 1) { 113 provider.get() 114 } else { 115 Any() 116 } 117 } 118 } 119 ) 120 .build() 121 assertThat(callCount.get()).isEqualTo(0) 122 val first = component.getReentrantObject() 123 assertThat(callCount.get()).isEqualTo(2) 124 val second = component.getReentrantObject() 125 assertThat(callCount.get()).isEqualTo(2) 126 assertThat(first).isSameInstanceAs(second) 127 } 128 129 @Test 130 fun testFailingReentrant() { 131 val callCount = AtomicInteger(0) 132 133 // Provides a failing reentrant binding. Even though it's scoped, the provides method is called 134 // twice. In this case we throw an exception since a different instance is provided on the 135 // second call. 136 val component = 137 DaggerDoubleCheckCycleTest_TestComponent.builder() 138 .overrideModule( 139 object : OverrideModule() { 140 override fun provideReentrantObject(provider: Provider<Any>): Any { 141 if (callCount.incrementAndGet() == 1) { 142 provider.get() 143 return Any() 144 } 145 return Any() 146 } 147 } 148 ) 149 .build() 150 assertThat(callCount.get()).isEqualTo(0) 151 try { 152 component.getReentrantObject() 153 fail("Expected IllegalStateException") 154 } catch (e: IllegalStateException) { 155 assertThat(e).hasMessageThat().contains("Scoped provider was invoked recursively") 156 } 157 assertThat(callCount.get()).isEqualTo(2) 158 } 159 160 @Test(timeout = 5000) 161 @Throws(Exception::class) 162 fun testGetFromMultipleThreads() { 163 val callCount = AtomicInteger(0) 164 val requestCount = AtomicInteger(0) 165 val future: SettableFuture<Any> = SettableFuture.create() 166 167 // Provides a non-reentrant binding. In this case, we return a SettableFuture so that we can 168 // control when the provides method returns. 169 val component = 170 DaggerDoubleCheckCycleTest_TestComponent.builder() 171 .overrideModule( 172 object : OverrideModule() { 173 override fun provideObject(): Any { 174 callCount.incrementAndGet() 175 return try { 176 Uninterruptibles.getUninterruptibly(future) 177 } catch (e: ExecutionException) { 178 throw RuntimeException(e) 179 } 180 } 181 } 182 ) 183 .build() 184 val numThreads = 10 185 val remainingTasks = CountDownLatch(numThreads) 186 val tasks: MutableList<Thread> = ArrayList(numThreads) 187 val values: MutableList<Any> = Collections.synchronizedList<Any>(ArrayList<Any>(numThreads)) 188 189 // Set up multiple threads that call component.getObject(). 190 for (i in 0 until numThreads) { 191 tasks.add( 192 Thread { 193 requestCount.incrementAndGet() 194 values.add(component.getObject()) 195 remainingTasks.countDown() 196 } 197 ) 198 } 199 200 // Check initial conditions 201 assertThat(remainingTasks.getCount()).isEqualTo(10) 202 assertThat(requestCount.get()).isEqualTo(0) 203 assertThat(callCount.get()).isEqualTo(0) 204 assertThat(values).isEmpty() 205 206 // Start all threads 207 tasks.forEach(Thread::start) 208 209 // Wait for all threads to wait/block. 210 var waiting: Long = 0L 211 while (waiting != numThreads.toLong()) { 212 waiting = 213 tasks.stream() 214 .map(Thread::getState) 215 .filter { state -> state == WAITING || state == BLOCKED } 216 .count() 217 } 218 219 // Check the intermediate state conditions. 220 // * All 10 threads should have requested the binding, but none should have finished. 221 // * Only 1 thread should have reached the provides method. 222 // * None of the threads should have set a value (since they are waiting for future to be set). 223 assertThat(remainingTasks.getCount()).isEqualTo(10) 224 assertThat(requestCount.get()).isEqualTo(10) 225 assertThat(callCount.get()).isEqualTo(1) 226 assertThat(values).isEmpty() 227 228 // Set the future and wait on all remaining threads to finish. 229 val futureValue = Any() 230 future.set(futureValue) 231 remainingTasks.await() 232 233 // Check the final state conditions. 234 // All values should be set now, and they should all be equal to the same instance. 235 assertThat(remainingTasks.getCount()).isEqualTo(0) 236 assertThat(requestCount.get()).isEqualTo(10) 237 assertThat(callCount.get()).isEqualTo(1) 238 assertThat(values).isEqualTo(Collections.nCopies(numThreads, futureValue)) 239 } 240 } 241