diff --git a/.changes/next-release/feature-AWSSDKforJavav2-f9cffed.json b/.changes/next-release/feature-AWSSDKforJavav2-f9cffed.json new file mode 100644 index 000000000000..bc9849d2fc90 --- /dev/null +++ b/.changes/next-release/feature-AWSSDKforJavav2-f9cffed.json @@ -0,0 +1,6 @@ +{ + "type": "feature", + "category": "AWS SDK for Java v2", + "contributor": "", + "description": "Added support for Waiters specifically for Matchers with Error to accept true/false value not as string but as boolean values such that True value is to match on any error code, or boolean false to test if no errors were encountered as per the SDK Waiter specs." +} diff --git a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterClassSpec.java b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterClassSpec.java index 45d361e7b2bb..09fe9b4fb2d6 100644 --- a/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterClassSpec.java +++ b/codegen/src/main/java/software/amazon/awssdk/codegen/poet/waiters/BaseWaiterClassSpec.java @@ -21,6 +21,7 @@ import static javax.lang.model.element.Modifier.STATIC; import static software.amazon.awssdk.utils.internal.CodegenNamingUtils.lowercaseFirstChar; +import com.fasterxml.jackson.jr.stree.JrsBoolean; import com.fasterxml.jackson.jr.stree.JrsString; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; @@ -477,9 +478,14 @@ private CodeBlock acceptor(Acceptor acceptor) { return CodeBlock.of("new $T($L, $T.$L)", waitersRuntimeClass().nestedClass("ResponseStatusAcceptor"), expected, WaiterState.class, waiterState(acceptor)); case "error": - result.add("OnExceptionAcceptor("); - result.add(errorAcceptorBody(acceptor)); - result.add(")"); + Boolean expectedBoolean = acceptorBooleanValue(acceptor); + if (expectedBoolean != null) { + result.add(booleanValueErrorBlock(acceptor, expectedBoolean).build()); + } else { + result.add("OnExceptionAcceptor("); + result.add(errorAcceptorBody(acceptor)); + result.add(")"); + } break; default: throw new IllegalArgumentException("Unsupported acceptor matcher: " + acceptor.getMatcher()); @@ -488,6 +494,27 @@ private CodeBlock acceptor(Acceptor acceptor) { return result.build(); } + private CodeBlock.Builder booleanValueErrorBlock(Acceptor acceptor, Boolean expectedBoolean) { + CodeBlock.Builder codeBlock = CodeBlock.builder(); + if (Boolean.FALSE.equals(expectedBoolean)) { + codeBlock.add("OnResponseAcceptor("); + codeBlock.add(trueForAllResponse()); + } + else { + codeBlock.add("OnExceptionAcceptor("); + codeBlock.add("error -> errorCode(error) " + (expectedBoolean ? "!=" : "==") + " null"); + } + codeBlock.add(")"); + return codeBlock; + } + + private static Boolean acceptorBooleanValue(Acceptor acceptor) { + if(acceptor.getExpected() instanceof JrsBoolean){ + return Boolean.parseBoolean(acceptor.getExpected().asText()); + } + return null; + } + private String waiterState(Acceptor acceptor) { switch (acceptor.getState()) { case "success": @@ -546,6 +573,12 @@ private CodeBlock pathAnyAcceptorBody(Acceptor acceptor) { .build(); } + private CodeBlock trueForAllResponse() { + return CodeBlock.builder() + .add("response -> true") + .build(); + } + private CodeBlock errorAcceptorBody(Acceptor acceptor) { String expected = acceptor.getExpected().asText(); String expectedType = acceptor.getExpected() instanceof JrsString ? "$S" : "$L"; diff --git a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json index b629e680af14..c8b62615fa63 100644 --- a/test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json +++ b/test/codegen-generated-classes-test/src/main/resources/codegen-resources/waiters/waiters-2.json @@ -27,6 +27,105 @@ "expected": 500 } ] + }, + "ErrorMatcherWithExpectedTrueFails": { + "delay": 1, + "operation": "AllTypes", + "maxAttempts": 40, + "acceptors": [ + { + "matcher": "error", + "expected": true, + "state": "failure" + } + ] + }, + "ErrorMatcherWithExpectedTrueAndStateAsSuccess": { + "delay": 1, + "operation": "AllTypes", + "maxAttempts": 40, + "acceptors": [ + { + "matcher" : "error", + "state" : "success", + "expected" : true + } + ] + }, + "ErrorMatcherWithExpectedFalseSuccess": { + "delay": 1, + "operation": "AllTypes", + "maxAttempts": 40, + "acceptors": [ + { + "matcher" : "error", + "state" : "success", + "expected" : false + } + ] + }, + "ErrorMatcherWithExpectedFalseFails": { + "delay": 1, + "operation": "AllTypes", + "maxAttempts": 40, + "acceptors": [ + { + "expected": false, + "matcher": "error", + "state": "failure" + } + ] + }, + "ErrorMatcherWithExpectedFalseRetries": { + "delay": 1, + "operation": "AllTypes", + "maxAttempts": 40, + "acceptors": [ + { + "matcher" : "error", + "state" : "retry", + "expected" : false + }, + { + "matcher": "error", + "expected": "EmptyModeledException", + "state": "success" + } + ] + }, + "SuccessMatcherWith200Pass404RetryErrorMatcherWithExpectedTrueFails": { + "delay": 1, + "operation": "AllTypes", + "maxAttempts": 40, + "acceptors": [ + { + "expected": 200, + "matcher": "status", + "state": "success" + }, + { + "state": "retry", + "matcher": "status", + "expected": 404 + }, + { + "matcher": "error", + "expected": true, + "state": "failure" + } + ] + }, + "ErrorMatcherWithExpectedFalseRetriesAndSuccessMatcherWith200Success": { + "delay": 1, + "operation": "AllTypes", + "maxAttempts": 40, + "acceptors": [ + { + "matcher" : "error", + "state" : "retry", + "expected" : true + } + ] } } } diff --git a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java index e645f3db903a..e32ad04d1bbe 100644 --- a/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java +++ b/test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/waiters/WaitersSyncFunctionalTest.java @@ -194,4 +194,196 @@ public void closeWaiterCreatedWithClient_clientDoesNotClose() { verify(client, never()).close(); } + @Test + public void errorMatcherWithExpectedTrueFails_withAPISuccess() { + AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder() + .sdkHttpResponse(SdkHttpResponse.builder() + .statusCode(200) + .build()) + .build(); + when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response); + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedTrueFails(SdkBuilder::build)) + .hasMessageContaining("The waiter has exceeded the max retry attempts: 3") + .isInstanceOf(SdkClientException.class); + } + + @Test + public void errorMatcherWithExpectedTrueFails_withAPIError() { + when(client.allTypes(any(AllTypesRequest.class))).thenThrow(EmptyModeledException.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .errorCode("EmptyModeledException") + .build()) + .build()); + + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedTrueFails(SdkBuilder::build)) + .hasMessageContaining("A waiter acceptor was matched and transitioned the waiter to failure state") + .isInstanceOf(SdkClientException.class); + } + + /** + * If we are not getting any errors then fail it { "expected": false, "matcher": "error", "state": "failure" } + */ + @Test + public void errorMatcherWithExpectedFalseFails() { + AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder() + .sdkHttpResponse(SdkHttpResponse.builder() + .statusCode(200) + .build()) + .build(); + when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response); + + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build)) + .hasMessageContaining("transitioned the waiter to failure state") + .isInstanceOf(SdkClientException.class); + } + + @Test + public void untilSuccessMatcherWith200Pass404RetryErrorMatcherWithExpectedTrueFails() { + AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder() + .sdkHttpResponse(SdkHttpResponse.builder() + .statusCode(200) + .build()) + .build(); + when(client.allTypes(any(AllTypesRequest.class))).thenThrow(SdkServiceException.builder().statusCode(404).build()) + .thenReturn(response); + WaiterResponse waiterResponse = + waiter.waitUntilSuccessMatcherWith200Pass404RetryErrorMatcherWithExpectedTrueFails(SdkBuilder::build); + assertThat(waiterResponse.attemptsExecuted()).isEqualTo(2); + } + + @Test + public void errorMatcherWithExpectedFalseSuccess_APISuccess() { + AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder() + .sdkHttpResponse(SdkHttpResponse.builder() + .statusCode(200) + .build()) + .build(); + when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response); + WaiterResponse allTypesResponseWaiterResponse = + waiter.waitUntilErrorMatcherWithExpectedFalseSuccess(SdkBuilder::build); + assertThat(allTypesResponseWaiterResponse.attemptsExecuted()).isEqualTo(1); + } + + // Error reported but not mentioned in Waiter acceptors + @Test + public void errorMatcherWithExpectedFalseSuccess_APIFailure() { + when(client.allTypes(any(AllTypesRequest.class))).thenThrow(SdkServiceException.builder().statusCode(404).build()); + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseSuccess(SdkBuilder::build)) + .hasMessageContaining("An exception was thrown and did not match any waiter acceptors") + .isInstanceOf(SdkClientException.class); + } + + @Test + public void errorMatcherWithExpectedTrueAndStateAsSuccess_ApiError() { + when(client.allTypes(any(AllTypesRequest.class))).thenThrow(EmptyModeledException.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .errorCode("EmptyModeledException") + .build()) + .build()); + WaiterResponse allTypesResponseWaiterResponse = + waiter.waitUntilErrorMatcherWithExpectedTrueAndStateAsSuccess(SdkBuilder::build); + assertThat(allTypesResponseWaiterResponse.attemptsExecuted()).isEqualTo(1); + } + + @Test + public void errorMatcherWithExpectedTrueAndStateAsSuccess_ApiSuccess() { + AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder() + .sdkHttpResponse(SdkHttpResponse.builder() + .statusCode(200) + .build()) + .build(); + when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response); + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedTrueAndStateAsSuccess(SdkBuilder::build)) + .hasMessageContaining("The waiter has exceeded the max retry attempts:") + .isInstanceOf(SdkClientException.class); + + } + + /** + * Case with the model just defined that it should Fail when there are no Errors But waitor does not tell what to do if there + * is a error. + */ + @Test + public void errorMatcherWithExpectedFalse_TerminatesWaitingIfErrorReported() { + when(client.allTypes(any(AllTypesRequest.class))).thenThrow(SdkServiceException.builder().statusCode(404).build()); + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build)) + .hasMessageContaining("An exception was thrown and did not match any waiter acceptors") + .isInstanceOf(SdkClientException.class); + + } + + @Test + public void errorMatcherWithExpectedFalseAndStateAsFailure_whenAPISuccess() { + AllTypesResponse response = (AllTypesResponse) AllTypesResponse.builder() + .sdkHttpResponse(SdkHttpResponse.builder() + .statusCode(200) + .build()) + .build(); + + + when(client.allTypes(any(AllTypesRequest.class))).thenReturn(response); + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build)) + .hasMessageContaining("A waiter acceptor was matched and transitioned the waiter to failure state") + .isInstanceOf(SdkClientException.class); + + } + + @Test + public void errorMatcherWithExpectedFalseAndStateAsFailure_whenAPIErrors() { + when(client.allTypes(any(AllTypesRequest.class))).thenThrow(EmptyModeledException.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .errorCode("EmptyModeledException") + .build()) + .build()); + + + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseFails(SdkBuilder::build)) + .hasMessageContaining("An exception was thrown and did not match any waiter acceptors") + .isInstanceOf(SdkClientException.class); + + } + + @Test + public void errorMatcherWithExpectedFalseRetries_exhaustAllRetries() { + AllTypesResponse response = + (AllTypesResponse) AllTypesResponse.builder() + .sdkHttpResponse(SdkHttpResponse.builder().statusCode(200) + .build()) + .build(); + when(client.allTypes(any(AllTypesRequest.class))) + .thenReturn(response); + assertThatThrownBy(() -> waiter.waitUntilErrorMatcherWithExpectedFalseRetries(AllTypesRequest.builder().build())) + .isInstanceOf(SdkClientException.class).hasMessageContaining("The waiter has exceeded the max retry attempts: 3"); + } + + /** + * This is a case were we want to check if an item is deleted + * In this case waiter first calls will say item exist + * and then its deleted it will throw exception + */ + @Test + public void errorMatcherWithExpectedFalseRetries_passesWhenApiReturnErrors() { + AllTypesResponse response = + (AllTypesResponse) AllTypesResponse.builder() + .sdkHttpResponse(SdkHttpResponse.builder() + .statusCode(200) + .build()) + .build(); + when(client.allTypes(any(AllTypesRequest.class))) + .thenReturn(response) + .thenReturn(response) + .thenThrow( + EmptyModeledException.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .errorCode("EmptyModeledException") + .build()) + .build() + + ); + WaiterResponse waiterResponse = + waiter.waitUntilErrorMatcherWithExpectedFalseRetries(AllTypesRequest.builder().build()); + assertThat(waiterResponse.attemptsExecuted()).isEqualTo(3); + // Empty because the waiter specifically waits for Error case. + assertThat(waiterResponse.matched().response()).isEmpty(); + } }