diff --git a/README.md b/README.md index dcb40a9cc..577bd7e03 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-datastore' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-datastore:2.19.0' +implementation 'com.google.cloud:google-cloud-datastore:2.19.1' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-datastore" % "2.19.0" +libraryDependencies += "com.google.cloud" % "google-cloud-datastore" % "2.19.1" ``` @@ -380,7 +380,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-datastore/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-datastore.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-datastore/2.19.0 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-datastore/2.19.1 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreException.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreException.java index 5d01c3b9d..d258feac8 100644 --- a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreException.java +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/DatastoreException.java @@ -16,16 +16,17 @@ package com.google.cloud.datastore; -import static com.google.cloud.BaseServiceException.isRetryable; - +import com.google.api.gax.grpc.GrpcStatusCode; import com.google.api.gax.rpc.ApiException; -import com.google.api.gax.rpc.ErrorDetails; +import com.google.api.gax.rpc.StatusCode; import com.google.cloud.BaseServiceException; import com.google.cloud.RetryHelper.RetryHelperException; -import com.google.cloud.grpc.BaseGrpcServiceException; +import com.google.cloud.http.BaseHttpServiceException; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; import java.io.IOException; -import java.util.Map; import java.util.Set; /** @@ -34,7 +35,7 @@ * @see Google Cloud * Datastore error codes */ -public final class DatastoreException extends BaseGrpcServiceException { +public final class DatastoreException extends BaseHttpServiceException { // see https://cloud.google.com/datastore/docs/concepts/errors#Error_Codes" private static final Set RETRYABLE_ERRORS = @@ -43,110 +44,106 @@ public final class DatastoreException extends BaseGrpcServiceException { new Error(4, "DEADLINE_EXCEEDED", false), new Error(14, "UNAVAILABLE", true)); private static final long serialVersionUID = 2663750991205874435L; - private String reason; - private ApiException apiException; public DatastoreException(int code, String message, String reason) { this(code, message, reason, true, null); - this.reason = reason; + } + + public DatastoreException(int code, String message, Throwable cause) { + super(code, message, null, true, RETRYABLE_ERRORS, cause); } public DatastoreException(int code, String message, String reason, Throwable cause) { - super(message, cause, code, isRetryable(code, reason, true, RETRYABLE_ERRORS)); - this.reason = reason; + super(code, message, reason, true, RETRYABLE_ERRORS, cause); } public DatastoreException( int code, String message, String reason, boolean idempotent, Throwable cause) { - super(message, cause, code, isRetryable(code, reason, idempotent, RETRYABLE_ERRORS)); - this.reason = reason; + super(code, message, reason, idempotent, RETRYABLE_ERRORS, cause); } public DatastoreException(IOException exception) { - super(exception, true); - } - - public DatastoreException(ApiException apiException) { - super(apiException); - this.apiException = apiException; + super(exception, true, RETRYABLE_ERRORS); } /** - * Checks the underlying reason of the exception and if it's {@link ApiException} then return the - * specific domain otherwise null. + * Translate RetryHelperException to the DatastoreException that caused the error. This method + * will always throw an exception. * - * @see Domain - * @return the logical grouping to which the "reason" belongs. + * @throws DatastoreException when {@code ex} was caused by a {@code DatastoreException} */ - public String getDomain() { - if (this.apiException != null) { - return this.apiException.getDomain(); - } - return null; + static DatastoreException translateAndThrow(RetryHelperException ex) { + BaseServiceException.translate(ex); + throw transformThrowable(ex); } - /** - * Checks the underlying reason of the exception and if it's {@link ApiException} then return a - * map of key-value pairs otherwise null. - * - * @see Metadata - * @return the map of additional structured details about an error. - */ - public Map getMetadata() { - if (this.apiException != null) { - return this.apiException.getMetadata(); + static BaseServiceException transformThrowable(Throwable t) { + if (t instanceof BaseServiceException) { + return (BaseServiceException) t; } - return null; + if (t.getCause() instanceof BaseServiceException) { + return (BaseServiceException) t.getCause(); + } + if (t instanceof ApiException) { + return asDatastoreException((ApiException) t); + } + if (t.getCause() instanceof ApiException) { + return asDatastoreException((ApiException) t.getCause()); + } + return getDatastoreException(t); } - /** - * Checks the underlying reason of the exception and if it's {@link ApiException} then return the - * ErrorDetails otherwise null. - * - * @see Status - * @see Error - * Details - * @return An object containing getters for structured objects from error_details.proto. - */ - public ErrorDetails getErrorDetails() { - if (this.apiException != null) { - return this.apiException.getErrorDetails(); + private static DatastoreException getDatastoreException(Throwable t) { + // unwrap a RetryHelperException if that is what is being translated + if (t instanceof RetryHelperException) { + return new DatastoreException(UNKNOWN_CODE, t.getMessage(), null, t.getCause()); } - return null; + return new DatastoreException(UNKNOWN_CODE, t.getMessage(), t); } - /** - * Checks the underlying reason of the exception and if it's {@link ApiException} then return the - * reason otherwise null/custom reason. - * - * @see Reason - * @return the reason of an error. - */ - @Override - public String getReason() { - if (this.apiException != null) { - return this.apiException.getReason(); + static DatastoreException asDatastoreException(ApiException apiEx) { + int datastoreStatusCode = 0; + StatusCode statusCode = apiEx.getStatusCode(); + if (statusCode instanceof GrpcStatusCode) { + GrpcStatusCode gsc = (GrpcStatusCode) statusCode; + datastoreStatusCode = + GrpcToDatastoreCodeTranslation.grpcCodeToDatastoreStatusCode(gsc.getTransportCode()); + } + + // If there is a gRPC exception in our cause, pull its error message up to be our + // message otherwise, create a generic error message with the status code. + String statusCodeName = statusCode.getCode().name(); + String statusExceptionMessage = getStatusExceptionMessage(apiEx); + + String message; + if (statusExceptionMessage != null) { + message = statusCodeName + ": " + statusExceptionMessage; + } else { + message = "Error: " + statusCodeName; + } + + String reason = ""; + if (Strings.isNullOrEmpty(apiEx.getReason())) { + if (apiEx.getStatusCode() != null) { + reason = apiEx.getStatusCode().getCode().name(); + } } - return this.reason; + // It'd be better to use ExceptionData and BaseServiceException#(ExceptionData) but, + // BaseHttpServiceException does not pass that through so we're stuck using this for now. + // TODO: When we can break the coupling to BaseHttpServiceException replace this + return new DatastoreException(datastoreStatusCode, message, reason, apiEx); } - /** - * Translate RetryHelperException to the DatastoreException that caused the error. This method - * will always throw an exception. - * - * @throws DatastoreException when {@code ex} was caused by a {@code DatastoreException} - */ - static DatastoreException translateAndThrow(RetryHelperException ex) { - BaseServiceException.translate(ex); - if (ex.getCause() instanceof ApiException) { - throw new DatastoreException((ApiException) ex.getCause()); + private static String getStatusExceptionMessage(Exception apiEx) { + if (apiEx.getMessage() != null) { + return apiEx.getMessage(); + } else { + Throwable cause = apiEx.getCause(); + if (cause instanceof StatusRuntimeException || cause instanceof StatusException) { + return cause.getMessage(); + } + return null; } - throw new DatastoreException(UNKNOWN_CODE, ex.getMessage(), null, ex.getCause()); } /** diff --git a/google-cloud-datastore/src/main/java/com/google/cloud/datastore/GrpcToDatastoreCodeTranslation.java b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/GrpcToDatastoreCodeTranslation.java new file mode 100644 index 000000000..1d63fb19a --- /dev/null +++ b/google-cloud-datastore/src/main/java/com/google/cloud/datastore/GrpcToDatastoreCodeTranslation.java @@ -0,0 +1,93 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore; + +import com.google.api.gax.grpc.GrpcStatusCode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.rpc.Code; +import io.grpc.Status; +import java.util.Map; +import java.util.function.Function; + +final class GrpcToDatastoreCodeTranslation { + /** Mappings between gRPC status codes and their corresponding code numbers. */ + private static final ImmutableList STATUS_CODE_MAPPINGS = + ImmutableList.of( + StatusCodeMapping.of(Code.OK.getNumber(), Status.Code.OK), + StatusCodeMapping.of(Code.DATA_LOSS.getNumber(), Status.Code.DATA_LOSS), + StatusCodeMapping.of(Code.INVALID_ARGUMENT.getNumber(), Status.Code.INVALID_ARGUMENT), + StatusCodeMapping.of(Code.OUT_OF_RANGE.getNumber(), Status.Code.OUT_OF_RANGE), + StatusCodeMapping.of(Code.UNAUTHENTICATED.getNumber(), Status.Code.UNAUTHENTICATED), + StatusCodeMapping.of(Code.PERMISSION_DENIED.getNumber(), Status.Code.PERMISSION_DENIED), + StatusCodeMapping.of(Code.NOT_FOUND.getNumber(), Status.Code.NOT_FOUND), + StatusCodeMapping.of(Code.ALREADY_EXISTS.getNumber(), Status.Code.ALREADY_EXISTS), + StatusCodeMapping.of( + Code.FAILED_PRECONDITION.getNumber(), Status.Code.FAILED_PRECONDITION), + StatusCodeMapping.of(Code.RESOURCE_EXHAUSTED.getNumber(), Status.Code.RESOURCE_EXHAUSTED), + StatusCodeMapping.of(Code.INTERNAL.getNumber(), Status.Code.INTERNAL), + StatusCodeMapping.of(Code.UNIMPLEMENTED.getNumber(), Status.Code.UNIMPLEMENTED), + StatusCodeMapping.of(Code.UNAVAILABLE.getNumber(), Status.Code.UNAVAILABLE), + StatusCodeMapping.of(Code.DEADLINE_EXCEEDED.getNumber(), Status.Code.DEADLINE_EXCEEDED), + StatusCodeMapping.of(Code.ABORTED.getNumber(), Status.Code.ABORTED), + StatusCodeMapping.of(Code.CANCELLED.getNumber(), Status.Code.CANCELLED), + StatusCodeMapping.of(Code.UNKNOWN.getNumber(), Status.Code.UNKNOWN)); + + /** Index our {@link StatusCodeMapping} for constant time lookup by {@link Status.Code} */ + private static final Map GRPC_CODE_INDEX = + STATUS_CODE_MAPPINGS.stream() + .collect( + ImmutableMap.toImmutableMap(StatusCodeMapping::getGrpcCode, Function.identity())); + + static int grpcCodeToDatastoreStatusCode(Status.Code code) { + StatusCodeMapping found = GRPC_CODE_INDEX.get(code); + // theoretically it's possible for gRPC to add a new code we haven't mapped here, if this + // happens fall through to our default of 0 + if (found != null) { + return found.getDatastoreCode(); + } else { + return 0; + } + } + + /** + * Simple tuple class to bind together our corresponding http status code and {@link Status.Code} + * while providing easy access to the correct {@link GrpcStatusCode} where necessary. + */ + private static final class StatusCodeMapping { + + private final int datastoreCode; + + private final Status.Code grpcCode; + + private StatusCodeMapping(int datastoreCode, Status.Code grpcCode) { + this.datastoreCode = datastoreCode; + this.grpcCode = grpcCode; + } + + public int getDatastoreCode() { + return datastoreCode; + } + + public Status.Code getGrpcCode() { + return grpcCode; + } + + static StatusCodeMapping of(int datastoreCode, Status.Code grpcCode) { + return new StatusCodeMapping(datastoreCode, grpcCode); + } + } +} diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreExceptionTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreExceptionTest.java index 33f5ebb9c..8c52b5519 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreExceptionTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/DatastoreExceptionTest.java @@ -16,9 +16,6 @@ package com.google.cloud.datastore; -import static com.google.common.truth.Truth.assertThat; -import static com.google.rpc.Code.FAILED_PRECONDITION; -import static java.util.Collections.singletonMap; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; @@ -27,20 +24,11 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import com.google.api.gax.grpc.GrpcStatusCode; -import com.google.api.gax.rpc.ApiException; -import com.google.api.gax.rpc.ApiExceptionFactory; -import com.google.api.gax.rpc.ErrorDetails; -import com.google.api.gax.rpc.StatusCode; import com.google.cloud.BaseServiceException; import com.google.cloud.RetryHelper; -import com.google.protobuf.Any; -import com.google.rpc.ErrorInfo; -import io.grpc.Status; import java.io.IOException; import java.net.SocketTimeoutException; import org.junit.Test; @@ -88,78 +76,39 @@ public void testDatastoreException() { assertEquals("message", exception.getMessage()); assertFalse(exception.isRetryable()); assertSame(cause, exception.getCause()); - - exception = new DatastoreException(2, "message", "INTERNAL", true, cause); - assertEquals(2, exception.getCode()); - assertEquals("INTERNAL", exception.getReason()); - assertEquals("message", exception.getMessage()); - assertFalse(exception.isRetryable()); - assertSame(cause, exception.getCause()); - - ApiException apiException = createApiException(); - exception = new DatastoreException(apiException); - assertEquals(400, exception.getCode()); - assertEquals("MISSING_INDEXES", exception.getReason()); - assertThat(exception.getMetadata()) - .isEqualTo(singletonMap("missing_indexes_url", "__some__url__")); - assertSame(apiException, exception.getCause()); - } - - @Test - public void testApiException() { - ApiException apiException = createApiException(); - DatastoreException datastoreException = new DatastoreException(apiException); - - assertThat(datastoreException.getReason()).isEqualTo("MISSING_INDEXES"); - assertThat(datastoreException.getDomain()).isEqualTo("datastore.googleapis.com"); - assertThat(datastoreException.getMetadata()) - .isEqualTo(singletonMap("missing_indexes_url", "__some__url__")); - assertThat(datastoreException.getErrorDetails()).isEqualTo(apiException.getErrorDetails()); } @Test public void testTranslateAndThrow() { Exception cause = new DatastoreException(14, "message", "UNAVAILABLE"); - final RetryHelper.RetryHelperException exceptionMock = + RetryHelper.RetryHelperException exceptionMock = createMock(RetryHelper.RetryHelperException.class); expect(exceptionMock.getCause()).andReturn(cause).times(2); replay(exceptionMock); - BaseServiceException ex = - assertThrows( - BaseServiceException.class, () -> DatastoreException.translateAndThrow(exceptionMock)); - assertEquals(14, ex.getCode()); - assertEquals("message", ex.getMessage()); - assertTrue(ex.isRetryable()); - verify(exceptionMock); - - cause = createApiException(); - final RetryHelper.RetryHelperException exceptionMock2 = - createMock(RetryHelper.RetryHelperException.class); - expect(exceptionMock2.getCause()).andReturn(cause).times(3); - replay(exceptionMock2); - DatastoreException ex2 = - assertThrows( - DatastoreException.class, () -> DatastoreException.translateAndThrow(exceptionMock2)); - assertThat(ex2.getReason()).isEqualTo("MISSING_INDEXES"); - assertThat(ex2.getDomain()).isEqualTo("datastore.googleapis.com"); - assertThat(ex2.getMetadata()).isEqualTo(singletonMap("missing_indexes_url", "__some__url__")); - assertThat(ex2.getErrorDetails()).isEqualTo(((ApiException) cause).getErrorDetails()); - verify(exceptionMock2); - + try { + DatastoreException.translateAndThrow(exceptionMock); + } catch (BaseServiceException ex) { + assertEquals(14, ex.getCode()); + assertEquals("message", ex.getMessage()); + assertTrue(ex.isRetryable()); + } finally { + verify(exceptionMock); + } cause = new IllegalArgumentException("message"); - final RetryHelper.RetryHelperException exceptionMock3 = - createMock(RetryHelper.RetryHelperException.class); - expect(exceptionMock3.getMessage()).andReturn("message").times(1); - expect(exceptionMock3.getCause()).andReturn(cause).times(3); - replay(exceptionMock3); - BaseServiceException ex3 = - assertThrows( - BaseServiceException.class, () -> DatastoreException.translateAndThrow(exceptionMock3)); - assertEquals(DatastoreException.UNKNOWN_CODE, ex3.getCode()); - assertEquals("message", ex3.getMessage()); - assertFalse(ex3.isRetryable()); - assertSame(cause, ex3.getCause()); - verify(exceptionMock3); + exceptionMock = createMock(RetryHelper.RetryHelperException.class); + expect(exceptionMock.getMessage()).andReturn("message").times(1); + expect(exceptionMock.getCause()).andReturn(cause).times(4); + replay(exceptionMock); + try { + DatastoreException.translateAndThrow(exceptionMock); + } catch (BaseServiceException ex) { + assertEquals(DatastoreException.UNKNOWN_CODE, ex.getCode()); + assertEquals("message", ex.getMessage()); + assertFalse(ex.isRetryable()); + assertSame(cause, ex.getCause()); + } finally { + verify(exceptionMock); + } } @Test @@ -172,26 +121,4 @@ public void testThrowInvalidRequest() { assertEquals("message a 1", ex.getMessage()); } } - - private ApiException createApiException() { - // Simulating google.rpc.Status with an ErrorInfo - ErrorInfo errorInfo = - ErrorInfo.newBuilder() - .setDomain("datastore.googleapis.com") - .setReason("MISSING_INDEXES") - .putMetadata("missing_indexes_url", "__some__url__") - .build(); - com.google.rpc.Status status = - com.google.rpc.Status.newBuilder() - .setCode(FAILED_PRECONDITION.getNumber()) - .setMessage("The query requires indexes.") - .addDetails(Any.pack(errorInfo)) - .build(); - - // Using Gax to convert to ApiException - StatusCode statusCode = GrpcStatusCode.of(Status.fromCodeValue(status.getCode()).getCode()); - ErrorDetails errorDetails = - ErrorDetails.builder().setRawErrorMessages(status.getDetailsList()).build(); - return ApiExceptionFactory.createException(null, statusCode, true, errorDetails); - } } diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/GrpcToDatastoreCodeTranslationTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/GrpcToDatastoreCodeTranslationTest.java new file mode 100644 index 000000000..3f297989f --- /dev/null +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/GrpcToDatastoreCodeTranslationTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.cloud.datastore; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.Status.Code; +import java.util.EnumMap; +import org.junit.Test; + +public class GrpcToDatastoreCodeTranslationTest { + @Test + public void grpcCodeToDatastoreCode_expectedMapping() { + EnumMap expected = new EnumMap<>(Code.class); + expected.put(Code.OK, com.google.rpc.Code.OK.getNumber()); + expected.put(Code.INVALID_ARGUMENT, com.google.rpc.Code.INVALID_ARGUMENT.getNumber()); + expected.put(Code.OUT_OF_RANGE, com.google.rpc.Code.OUT_OF_RANGE.getNumber()); + expected.put(Code.UNAUTHENTICATED, com.google.rpc.Code.UNAUTHENTICATED.getNumber()); + expected.put(Code.PERMISSION_DENIED, com.google.rpc.Code.PERMISSION_DENIED.getNumber()); + expected.put(Code.NOT_FOUND, com.google.rpc.Code.NOT_FOUND.getNumber()); + expected.put(Code.FAILED_PRECONDITION, com.google.rpc.Code.FAILED_PRECONDITION.getNumber()); + expected.put(Code.ALREADY_EXISTS, com.google.rpc.Code.ALREADY_EXISTS.getNumber()); + expected.put(Code.RESOURCE_EXHAUSTED, com.google.rpc.Code.RESOURCE_EXHAUSTED.getNumber()); + expected.put(Code.INTERNAL, com.google.rpc.Code.INTERNAL.getNumber()); + expected.put(Code.UNIMPLEMENTED, com.google.rpc.Code.UNIMPLEMENTED.getNumber()); + expected.put(Code.UNAVAILABLE, com.google.rpc.Code.UNAVAILABLE.getNumber()); + expected.put(Code.ABORTED, com.google.rpc.Code.ABORTED.getNumber()); + expected.put(Code.CANCELLED, com.google.rpc.Code.CANCELLED.getNumber()); + expected.put(Code.UNKNOWN, com.google.rpc.Code.UNKNOWN.getNumber()); + expected.put(Code.DEADLINE_EXCEEDED, com.google.rpc.Code.DEADLINE_EXCEEDED.getNumber()); + expected.put(Code.DATA_LOSS, com.google.rpc.Code.DATA_LOSS.getNumber()); + + EnumMap actual = new EnumMap<>(Code.class); + for (Code c : Code.values()) { + actual.put(c, GrpcToDatastoreCodeTranslation.grpcCodeToDatastoreStatusCode(c)); + } + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/AbstractITDatastoreTest.java b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/AbstractITDatastoreTest.java index 7c105672c..ac9587b2a 100644 --- a/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/AbstractITDatastoreTest.java +++ b/google-cloud-datastore/src/test/java/com/google/cloud/datastore/it/AbstractITDatastoreTest.java @@ -16,7 +16,11 @@ package com.google.cloud.datastore.it; +import static com.google.api.gax.rpc.StatusCode.Code.ALREADY_EXISTS; +import static com.google.api.gax.rpc.StatusCode.Code.DEADLINE_EXCEEDED; +import static com.google.api.gax.rpc.StatusCode.Code.FAILED_PRECONDITION; import static com.google.api.gax.rpc.StatusCode.Code.INVALID_ARGUMENT; +import static com.google.api.gax.rpc.StatusCode.Code.NOT_FOUND; import static com.google.cloud.datastore.aggregation.Aggregation.avg; import static com.google.cloud.datastore.aggregation.Aggregation.count; import static com.google.cloud.datastore.aggregation.Aggregation.sum; @@ -28,8 +32,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import com.google.cloud.Timestamp; import com.google.cloud.Tuple; @@ -70,9 +74,9 @@ import com.google.cloud.datastore.TimestampValue; import com.google.cloud.datastore.Transaction; import com.google.cloud.datastore.ValueType; -import com.google.cloud.grpc.GrpcTransportOptions; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.google.common.truth.Truth; import com.google.datastore.v1.TransactionOptions; import com.google.datastore.v1.TransactionOptions.ReadOnly; import java.util.ArrayList; @@ -383,19 +387,8 @@ public void testNewTransactionCommit() { assertEquals(ENTITY3, list.get(2)); assertEquals(3, list.size()); - try { - transaction.commit(); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - assertEquals("FAILED_PRECONDITION", expected.getReason()); - } - - try { - transaction.rollback(); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - assertEquals("FAILED_PRECONDITION", expected.getReason()); - } + DatastoreException expected = assertThrows(DatastoreException.class, transaction::commit); + assertDatastoreException(expected, FAILED_PRECONDITION.name(), 0); } @Test @@ -492,12 +485,8 @@ public void testNewTransactionRollback() { transaction.rollback(); transaction.rollback(); // should be safe to repeat rollback calls - try { - transaction.commit(); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - assertEquals("FAILED_PRECONDITION", expected.getReason()); - } + DatastoreException expected = assertThrows(DatastoreException.class, transaction::commit); + assertDatastoreException(expected, FAILED_PRECONDITION.name(), 0); List list = datastore.fetch(KEY1, KEY2, KEY3); assertEquals(ENTITY1, list.get(0)); @@ -544,12 +533,8 @@ public void testNewBatch() { assertEquals(PARTIAL_ENTITY3.getNames(), datastore.get(generatedKeys.get(0)).getNames()); assertEquals(PARTIAL_ENTITY3.getKey(), IncompleteKey.newBuilder(generatedKeys.get(0)).build()); - try { - batch.submit(); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - assertEquals("FAILED_PRECONDITION", expected.getReason()); - } + DatastoreException expected = assertThrows(DatastoreException.class, batch::submit); + assertDatastoreException(expected, FAILED_PRECONDITION.name(), 0); batch = datastore.newBatch(); batch.delete(entity4.getKey(), entity5.getKey(), entity6.getKey()); @@ -1256,12 +1241,11 @@ public void testGetArrayNoDeferredResults() { assertEquals(EMPTY_LIST_VALUE, entity3.getValue("emptyList")); assertEquals(8, entity3.getNames().size()); assertFalse(entity3.contains("bla")); - try { - entity3.getString("str"); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - // expected - no such property - } + + DatastoreException expected = + assertThrows(DatastoreException.class, () -> entity3.getString("str")); + assertDatastoreException(expected, FAILED_PRECONDITION.name(), 0); + assertFalse(result.hasNext()); datastore.delete(ENTITY3.getKey()); } @@ -1273,12 +1257,9 @@ public void testAddEntity() { assertNull(keys.get(1)); assertEquals(2, keys.size()); - try { - datastore.add(ENTITY1); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - // expected; - } + DatastoreException expected = + assertThrows(DatastoreException.class, () -> datastore.add(ENTITY1)); + assertDatastoreException(expected, ALREADY_EXISTS.name(), 6); List entities = datastore.add(ENTITY3, PARTIAL_ENTITY1, PARTIAL_ENTITY2); assertEquals(ENTITY3, datastore.get(ENTITY3.getKey())); @@ -1301,12 +1282,10 @@ public void testUpdate() { assertNull(keys.get(1)); assertEquals(2, keys.size()); - try { - datastore.update(ENTITY3); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - // expected; - } + DatastoreException expected = + assertThrows(DatastoreException.class, () -> datastore.update(ENTITY3)); + assertDatastoreException(expected, NOT_FOUND.name(), 5); + datastore.add(ENTITY3); assertEquals(ENTITY3, datastore.get(ENTITY3.getKey())); Entity entity3 = Entity.newBuilder(ENTITY3).clear().set("bla", new NullValue()).build(); @@ -1316,6 +1295,12 @@ public void testUpdate() { datastore.delete(ENTITY3.getKey()); } + private void assertDatastoreException( + DatastoreException expected, String reason, int datastoreStatusCode) { + Truth.assertThat(expected.getReason()).isEqualTo(reason); + Truth.assertThat(expected.getCode()).isEqualTo(datastoreStatusCode); + } + @Test public void testPut() { Entity updatedEntity = Entity.newBuilder(ENTITY1).set("new_property", 42L).build(); @@ -1392,12 +1377,9 @@ public Integer run(DatastoreReaderWriter transaction) { } }; - try { - datastore.runInTransaction(callable2); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - assertEquals(4, ((DatastoreException) expected.getCause()).getCode()); - } + DatastoreException expected = + assertThrows(DatastoreException.class, () -> datastore.runInTransaction(callable2)); + assertDatastoreException((DatastoreException) expected.getCause(), DEADLINE_EXCEEDED.name(), 4); } @Test @@ -1446,18 +1428,10 @@ public Integer run(DatastoreReaderWriter transaction) { .setReadOnly(TransactionOptions.ReadOnly.getDefaultInstance()) .build(); - try { - datastore.runInTransaction(callable2, readOnlyOptions); - fail("Expecting a failure"); - } catch (DatastoreException expected) { - if (datastore.getOptions().getTransportOptions() instanceof GrpcTransportOptions) { - assertEquals( - INVALID_ARGUMENT.getHttpStatusCode(), - ((DatastoreException) expected.getCause()).getCode()); - } else { - assertEquals(3, ((DatastoreException) expected.getCause()).getCode()); - } - } + DatastoreException expected = + assertThrows( + DatastoreException.class, () -> datastore.runInTransaction(callable2, readOnlyOptions)); + assertDatastoreException((DatastoreException) expected.getCause(), INVALID_ARGUMENT.name(), 3); } @Test