@@ -7,6 +7,8 @@ import io.sentry.SpanDataConvention
77import io.sentry.SpanStatus
88import io.sentry.TransactionContext
99import java.util.concurrent.Callable
10+ import java.util.concurrent.CompletableFuture
11+ import java.util.function.Supplier
1012import kotlin.test.BeforeTest
1113import kotlin.test.Test
1214import kotlin.test.assertEquals
@@ -137,6 +139,172 @@ class SentryCacheWrapperTest {
137139 assertEquals(false , tx.spans.first().getData(SpanDataConvention .CACHE_HIT_KEY ))
138140 }
139141
142+ // -- retrieve(Object key) --
143+
144+ @Test
145+ fun `retrieve creates span with cache hit true when future resolves with value` () {
146+ val tx = createTransaction()
147+ val wrapper = SentryCacheWrapper (delegate, scopes)
148+ whenever(delegate.retrieve(" myKey" )).thenReturn(CompletableFuture .completedFuture(" value" ))
149+
150+ val result = wrapper.retrieve(" myKey" )
151+
152+ assertEquals(" value" , result!! .get())
153+ assertEquals(1 , tx.spans.size)
154+ val span = tx.spans.first()
155+ assertEquals(" cache.get" , span.operation)
156+ assertEquals(" myKey" , span.description)
157+ assertEquals(SpanStatus .OK , span.status)
158+ assertEquals(true , span.getData(SpanDataConvention .CACHE_HIT_KEY ))
159+ assertTrue(span.isFinished)
160+ }
161+
162+ @Test
163+ fun `retrieve creates span with cache hit false when future resolves with null` () {
164+ val tx = createTransaction()
165+ val wrapper = SentryCacheWrapper (delegate, scopes)
166+ whenever(delegate.retrieve(" myKey" )).thenReturn(CompletableFuture .completedFuture(null ))
167+
168+ val result = wrapper.retrieve(" myKey" )
169+
170+ assertNull(result!! .get())
171+ assertEquals(1 , tx.spans.size)
172+ assertEquals(false , tx.spans.first().getData(SpanDataConvention .CACHE_HIT_KEY ))
173+ assertTrue(tx.spans.first().isFinished)
174+ }
175+
176+ @Test
177+ fun `retrieve creates span with cache hit false when delegate returns null` () {
178+ val tx = createTransaction()
179+ val wrapper = SentryCacheWrapper (delegate, scopes)
180+ whenever(delegate.retrieve(" myKey" )).thenReturn(null )
181+
182+ val result = wrapper.retrieve(" myKey" )
183+
184+ assertNull(result)
185+ assertEquals(1 , tx.spans.size)
186+ val span = tx.spans.first()
187+ assertEquals(false , span.getData(SpanDataConvention .CACHE_HIT_KEY ))
188+ assertEquals(SpanStatus .OK , span.status)
189+ assertTrue(span.isFinished)
190+ }
191+
192+ @Test
193+ fun `retrieve sets error status when future completes exceptionally` () {
194+ val tx = createTransaction()
195+ val wrapper = SentryCacheWrapper (delegate, scopes)
196+ val exception = RuntimeException (" async cache error" )
197+ whenever(delegate.retrieve(" myKey" ))
198+ .thenReturn(CompletableFuture <Any >().also { it.completeExceptionally(exception) })
199+
200+ val result = wrapper.retrieve(" myKey" )
201+
202+ assertFailsWith<Exception > { result!! .get() }
203+ assertEquals(1 , tx.spans.size)
204+ val span = tx.spans.first()
205+ assertEquals(SpanStatus .INTERNAL_ERROR , span.status)
206+ assertEquals(exception, span.throwable)
207+ assertTrue(span.isFinished)
208+ }
209+
210+ @Test
211+ fun `retrieve sets error status when delegate throws synchronously` () {
212+ val tx = createTransaction()
213+ val wrapper = SentryCacheWrapper (delegate, scopes)
214+ val exception = RuntimeException (" sync error" )
215+ whenever(delegate.retrieve(" myKey" )).thenThrow(exception)
216+
217+ assertFailsWith<RuntimeException > { wrapper.retrieve(" myKey" ) }
218+
219+ assertEquals(1 , tx.spans.size)
220+ val span = tx.spans.first()
221+ assertEquals(SpanStatus .INTERNAL_ERROR , span.status)
222+ assertEquals(exception, span.throwable)
223+ assertTrue(span.isFinished)
224+ }
225+
226+ @Test
227+ fun `retrieve does not create span when tracing is disabled` () {
228+ options.isEnableCacheTracing = false
229+ val tx = createTransaction()
230+ val wrapper = SentryCacheWrapper (delegate, scopes)
231+ whenever(delegate.retrieve(" myKey" )).thenReturn(CompletableFuture .completedFuture(" value" ))
232+
233+ wrapper.retrieve(" myKey" )
234+
235+ verify(delegate).retrieve(" myKey" )
236+ assertEquals(0 , tx.spans.size)
237+ }
238+
239+ // -- retrieve(Object key, Supplier<CompletableFuture<T>>) --
240+
241+ @Test
242+ fun `retrieve with loader creates span with cache hit true when loader not invoked` () {
243+ val tx = createTransaction()
244+ val wrapper = SentryCacheWrapper (delegate, scopes)
245+ // Simulate cache hit: delegate returns value without invoking the loader
246+ whenever(delegate.retrieve(eq(" myKey" ), any<Supplier <CompletableFuture <String >>>()))
247+ .thenReturn(CompletableFuture .completedFuture(" cached" ))
248+
249+ val result = wrapper.retrieve(" myKey" ) { CompletableFuture .completedFuture(" loaded" ) }
250+
251+ assertEquals(" cached" , result.get())
252+ assertEquals(1 , tx.spans.size)
253+ assertEquals(true , tx.spans.first().getData(SpanDataConvention .CACHE_HIT_KEY ))
254+ assertTrue(tx.spans.first().isFinished)
255+ }
256+
257+ @Test
258+ fun `retrieve with loader creates span with cache hit false when loader invoked` () {
259+ val tx = createTransaction()
260+ val wrapper = SentryCacheWrapper (delegate, scopes)
261+ // Simulate cache miss: delegate invokes the loader supplier
262+ whenever(delegate.retrieve(eq(" myKey" ), any<Supplier <CompletableFuture <String >>>()))
263+ .thenAnswer { invocation ->
264+ val loader = invocation.getArgument<Supplier <CompletableFuture <String >>>(1 )
265+ loader.get()
266+ }
267+
268+ val result = wrapper.retrieve(" myKey" ) { CompletableFuture .completedFuture(" loaded" ) }
269+
270+ assertEquals(" loaded" , result.get())
271+ assertEquals(1 , tx.spans.size)
272+ assertEquals(false , tx.spans.first().getData(SpanDataConvention .CACHE_HIT_KEY ))
273+ assertTrue(tx.spans.first().isFinished)
274+ }
275+
276+ @Test
277+ fun `retrieve with loader sets error status when future completes exceptionally` () {
278+ val tx = createTransaction()
279+ val wrapper = SentryCacheWrapper (delegate, scopes)
280+ val exception = RuntimeException (" async loader error" )
281+ whenever(delegate.retrieve(eq(" myKey" ), any<Supplier <CompletableFuture <String >>>()))
282+ .thenReturn(CompletableFuture <String >().also { it.completeExceptionally(exception) })
283+
284+ val result = wrapper.retrieve(" myKey" ) { CompletableFuture .completedFuture(" loaded" ) }
285+
286+ assertFailsWith<Exception > { result.get() }
287+ assertEquals(1 , tx.spans.size)
288+ val span = tx.spans.first()
289+ assertEquals(SpanStatus .INTERNAL_ERROR , span.status)
290+ assertEquals(exception, span.throwable)
291+ assertTrue(span.isFinished)
292+ }
293+
294+ @Test
295+ fun `retrieve with loader does not create span when tracing is disabled` () {
296+ options.isEnableCacheTracing = false
297+ val tx = createTransaction()
298+ val wrapper = SentryCacheWrapper (delegate, scopes)
299+ whenever(delegate.retrieve(eq(" myKey" ), any<Supplier <CompletableFuture <String >>>()))
300+ .thenReturn(CompletableFuture .completedFuture(" cached" ))
301+
302+ wrapper.retrieve(" myKey" ) { CompletableFuture .completedFuture(" loaded" ) }
303+
304+ verify(delegate).retrieve(eq(" myKey" ), any<Supplier <CompletableFuture <String >>>())
305+ assertEquals(0 , tx.spans.size)
306+ }
307+
140308 // -- put --
141309
142310 @Test
0 commit comments