@@ -5,12 +5,15 @@ package software.aws.toolkits.jetbrains.services.amazonq.clients
55
66import com.intellij.testFramework.RuleChain
77import com.intellij.testFramework.replaceService
8+ import kotlinx.coroutines.runBlocking
89import kotlinx.coroutines.test.runTest
10+ import org.assertj.core.api.Assertions.assertThat
911import org.junit.Before
1012import org.junit.Rule
1113import org.junit.Test
1214import org.mockito.kotlin.any
1315import org.mockito.kotlin.argumentCaptor
16+ import org.mockito.kotlin.doAnswer
1417import org.mockito.kotlin.doReturn
1518import org.mockito.kotlin.mock
1619import org.mockito.kotlin.stub
@@ -20,6 +23,7 @@ import software.amazon.awssdk.services.codewhispererstreaming.CodeWhispererStrea
2023import software.amazon.awssdk.services.codewhispererstreaming.model.ExportIntent
2124import software.amazon.awssdk.services.codewhispererstreaming.model.ExportResultArchiveRequest
2225import software.amazon.awssdk.services.codewhispererstreaming.model.ExportResultArchiveResponseHandler
26+ import software.amazon.awssdk.services.codewhispererstreaming.model.ValidationException
2327import software.amazon.awssdk.services.ssooidc.SsoOidcClient
2428import software.aws.toolkits.core.TokenConnectionSettings
2529import software.aws.toolkits.core.utils.test.aString
@@ -81,4 +85,156 @@ class AmazonQStreamingClientTest : AmazonQTestBase() {
8185 verify(streamingBearerClient).exportResultArchive(requestCaptor.capture(), handlerCaptor.capture())
8286 }
8387 }
88+
89+ @Test
90+ fun `verify retry on ValidationException` (): Unit = runBlocking {
91+ var attemptCount = 0
92+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
93+ on {
94+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
95+ } doAnswer {
96+ attemptCount++
97+ if (attemptCount <= 2 ) {
98+ CompletableFuture .runAsync {
99+ throw VALIDATION_EXCEPTION
100+ }
101+ } else {
102+ CompletableFuture .completedFuture(mock())
103+ }
104+ }
105+ }
106+
107+ amazonQStreamingClient.exportResultArchive(" test-id" , ExportIntent .TRANSFORMATION , null , {}, {})
108+
109+ assertThat(attemptCount).isEqualTo(3 )
110+ }
111+
112+ @Test
113+ fun `verify retry gives up after max attempts` (): Unit = runBlocking {
114+ var attemptCount = 0
115+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
116+ on {
117+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
118+ } doAnswer {
119+ attemptCount++
120+ CompletableFuture .runAsync {
121+ throw VALIDATION_EXCEPTION
122+ }
123+ }
124+ }
125+
126+ val thrown = catchCoroutineException {
127+ amazonQStreamingClient.exportResultArchive(" test-id" , ExportIntent .TRANSFORMATION , null , {}, {})
128+ }
129+
130+ assertThat(attemptCount).isEqualTo(3 )
131+ assertThat(thrown)
132+ .isInstanceOf(ValidationException ::class .java)
133+ .hasMessage(" Resource validation failed" )
134+ }
135+
136+ @Test
137+ fun `verify no retry on non-retryable exception` (): Unit = runBlocking {
138+ var attemptCount = 0
139+
140+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
141+ on {
142+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
143+ } doAnswer {
144+ attemptCount++
145+ CompletableFuture .runAsync {
146+ throw IllegalArgumentException (" Non-retryable error" )
147+ }
148+ }
149+ }
150+
151+ val thrown = catchCoroutineException {
152+ amazonQStreamingClient.exportResultArchive(" test-id" , ExportIntent .TRANSFORMATION , null , {}, {})
153+ }
154+
155+ assertThat(attemptCount).isEqualTo(1 )
156+ assertThat(thrown)
157+ .isInstanceOf(IllegalArgumentException ::class .java)
158+ .hasMessage(" Non-retryable error" )
159+ }
160+
161+ @Test
162+ fun `verify backoff timing between retries` (): Unit = runBlocking {
163+ var lastAttemptTime = 0L
164+ var minBackoffObserved = Long .MAX_VALUE
165+ var maxBackoffObserved = 0L
166+
167+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
168+ on {
169+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
170+ } doAnswer {
171+ val currentTime = System .currentTimeMillis()
172+ if (lastAttemptTime > 0 ) {
173+ val backoffTime = currentTime - lastAttemptTime
174+ minBackoffObserved = minOf(minBackoffObserved, backoffTime)
175+ maxBackoffObserved = maxOf(maxBackoffObserved, backoffTime)
176+ }
177+ lastAttemptTime = currentTime
178+
179+ CompletableFuture .runAsync {
180+ throw VALIDATION_EXCEPTION
181+ }
182+ }
183+ }
184+
185+ val thrown = catchCoroutineException {
186+ amazonQStreamingClient.exportResultArchive(" test-id" , ExportIntent .TRANSFORMATION , null , {}, {})
187+ }
188+
189+ assertThat(thrown)
190+ .isInstanceOf(ValidationException ::class .java)
191+ .hasMessage(" Resource validation failed" )
192+ assertThat(minBackoffObserved).isGreaterThanOrEqualTo(100 )
193+ assertThat(maxBackoffObserved).isLessThanOrEqualTo(10000 )
194+ }
195+
196+ @Test
197+ fun `verify onError callback is called with final exception` (): Unit = runBlocking {
198+ var errorCaught: Exception ? = null
199+
200+ streamingBearerClient = mockClientManagerRule.create<CodeWhispererStreamingAsyncClient >().stub {
201+ on {
202+ exportResultArchive(any<ExportResultArchiveRequest >(), any<ExportResultArchiveResponseHandler >())
203+ } doAnswer {
204+ CompletableFuture .runAsync {
205+ throw VALIDATION_EXCEPTION
206+ }
207+ }
208+ }
209+
210+ val thrown = catchCoroutineException {
211+ amazonQStreamingClient.exportResultArchive(
212+ " test-id" ,
213+ ExportIntent .TRANSFORMATION ,
214+ null ,
215+ { errorCaught = it },
216+ {}
217+ )
218+ }
219+
220+ assertThat(thrown)
221+ .isInstanceOf(ValidationException ::class .java)
222+ .hasMessage(" Resource validation failed" )
223+ assertThat(errorCaught).isEqualTo(VALIDATION_EXCEPTION )
224+ }
225+
226+ private suspend fun catchCoroutineException (block : suspend () -> Unit ): Throwable {
227+ try {
228+ block()
229+ error(" Expected exception was not thrown" )
230+ } catch (e: Throwable ) {
231+ return e
232+ }
233+ }
234+
235+ companion object {
236+ private val VALIDATION_EXCEPTION = ValidationException .builder()
237+ .message(" Resource validation failed" )
238+ .build()
239+ }
84240}
0 commit comments