diff --git a/CHANGELOG.md b/CHANGELOG.md index 6975f6d1f1..1bfcee6acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### [2.1.4](https://www.github.com/googleapis/java-storage/compare/v2.1.3...v2.1.4) (2021-09-20) + + +### Dependencies + +* update dependency com.google.apis:google-api-services-storage to v1-rev20210914-1.32.1 ([#1025](https://www.github.com/googleapis/java-storage/issues/1025)) ([ff56d5e](https://www.github.com/googleapis/java-storage/commit/ff56d5e5632d925542ac918d293b68dfcb32b465)) +* update kms.version to v0.92.1 ([#1023](https://www.github.com/googleapis/java-storage/issues/1023)) ([ca1afcf](https://www.github.com/googleapis/java-storage/commit/ca1afcff085bd02b150b93128b102cb9a61e1b4d)) + ### [2.1.3](https://www.github.com/googleapis/java-storage/compare/v2.1.2...v2.1.3) (2021-09-15) diff --git a/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml index 5a5fe8dc5c..0253d6979e 100644 --- a/gapic-google-cloud-storage-v2/pom.xml +++ b/gapic-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc gapic-google-cloud-storage-v2 - 2.1.3-alpha + 2.1.4-alpha gapic-google-cloud-storage-v2 GRPC library for gapic-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.1.3 + 2.1.4 diff --git a/google-cloud-storage/assets/retry-conformance-tests-diagram.png b/google-cloud-storage/assets/retry-conformance-tests-diagram.png new file mode 100644 index 0000000000..e0051e9c78 Binary files /dev/null and b/google-cloud-storage/assets/retry-conformance-tests-diagram.png differ diff --git a/google-cloud-storage/assets/retry-conformance-tests-diagram.txt b/google-cloud-storage/assets/retry-conformance-tests-diagram.txt new file mode 100644 index 0000000000..1146f2e2ab --- /dev/null +++ b/google-cloud-storage/assets/retry-conformance-tests-diagram.txt @@ -0,0 +1,59 @@ +# This is a text representation of retry-conformance-tests.diagram.png generated +# using https://www.websequencediagrams.com/ + +participant ITRetryConformanceTest +participant ITRetryConformanceTest.Static +participant RetryTestCaseResolver +participant GracefulConformanceEnforcement +participant RetryTestFixture +participant TestBench +participant Docker +participant RpcMethodMappings + +ITRetryConformanceTest->+ITRetryConformanceTest.Static: testCases + ITRetryConformanceTest.Static->RpcMethodMappings: + ITRetryConformanceTest.Static->+RetryTestCaseResolver: getRetryTestCases + RetryTestCaseResolver->RetryTestCaseResolver: loadRetryTestDefinitions + RetryTestCaseResolver->RetryTestCaseResolver: generateTestCases + RetryTestCaseResolver->RetryTestCaseResolver: shuffle + RetryTestCaseResolver->RetryTestCaseResolver: validateGeneratedTestCases + RetryTestCaseResolver->-ITRetryConformanceTest.Static: +ITRetryConformanceTest.Static->-ITRetryConformanceTest: + +ITRetryConformanceTest->+TestBench: apply + TestBench->TestBench: mktemp stdout + TestBench->TestBench: mktemp stderr + TestBench->+Docker: pull + Docker->-TestBench: + TestBench->+Docker: run + TestBench->+TestBench: await testbench up + TestBench->+Docker: GET /retry_tests + Docker->-TestBench: + deactivate TestBench + loop forEach test + ITRetryConformanceTest->+GracefulConformanceEnforcement: apply + ITRetryConformanceTest->+RetryTestFixture: apply + RetryTestFixture->+TestBench: createRetryTest + TestBench->+Docker: POST /retry_test + Docker->-TestBench: + TestBench->-RetryTestFixture: + ITRetryConformanceTest->ITRetryConformanceTest: test + RetryTestFixture->+TestBench: getRetryTest + TestBench->+Docker: GET /retry_test/{id} + Docker->-TestBench: + TestBench->-RetryTestFixture: + RetryTestFixture->RetryTestFixture: assert completion + RetryTestFixture->+TestBench: deleteRetryTest + TestBench->+Docker: DELETE /retry_test/{id} + Docker->-TestBench: + TestBench->-RetryTestFixture: + RetryTestFixture->-ITRetryConformanceTest: + opt if running in CI + GracefulConformanceEnforcement->GracefulConformanceEnforcement: check allow list + end + GracefulConformanceEnforcement->-ITRetryConformanceTest: + end + Docker->-TestBench: docker stop + TestBench->TestBench: rmtemp stdout + TestBench->TestBench: rmtemp stderr +TestBench->-ITRetryConformanceTest: diff --git a/google-cloud-storage/conformance-testing.md b/google-cloud-storage/conformance-testing.md new file mode 100644 index 0000000000..6b75bb502a --- /dev/null +++ b/google-cloud-storage/conformance-testing.md @@ -0,0 +1,45 @@ +# Conformance Testing + +This library leverages the conformance tests defined in [googleapis/conformance-tests](https://github.com/googleapis/conformance-tests) +to ensure adherence to expected behaviors. + +Access to the conformance tests is achieved via dependencies on +[`com.google.cloud:google-cloud-conformance-tests`](https://github.com/googleapis/java-conformance-tests) +which contains all generated protos and associated files necessary for loading +and accessing the tests. + +## Running the Conformance Tests + +Conformance tests are written and run as part of the JUnit tests suite. + +## Suites + +### Automatic Retries + +The JUnit tests class is [`ITRetryConformanceTest.java`](./src/test/java/com/google/cloud/storage/conformance/retry/ITRetryConformanceTest.java) +and is considered part of the integration test suite. + +This tests suite ensures that automatic retries for operations are properly defined +and handled to ensure data integrity. + +#### Prerequisites +1. Java 8+ +2. Maven +3. Docker (Docker for MacOS has been tested and verified to work as well) + +#### Test Suite Overview + +The test suite uses the [storage-testbench](https://github.com/googleapis/storage-testbench) +to configure and generate tests cases which use fault injection to ensure conformance. + +`ITRetryConformanceTest` encapsulates all the necessary lifecycle points needed +to run the test suite, including: +1. Running the testbench server via docker +2. Setup, validation, cleanup of individual test cases with the testbench +3. CI Graceful enforcement of test failures (enforce no regressions, but allow + for some cases to not pass without failing the whole run) + +A sequence diagram of how the tests are loaded run, and interact with testbench +can be seen below. Time moves from top to bottom, while component interactions +are shown via arrows laterally. +![](./assets/retry-conformance-tests-diagram.png) diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml index 9f04bb4886..5717677d27 100644 --- a/google-cloud-storage/pom.xml +++ b/google-cloud-storage/pom.xml @@ -2,7 +2,7 @@ 4.0.0 google-cloud-storage - 2.1.3 + 2.1.4 jar Google Cloud Storage https://github.com/googleapis/java-storage @@ -12,11 +12,11 @@ com.google.cloud google-cloud-storage-parent - 2.1.3 + 2.1.4 google-cloud-storage - 0.92.0 + 0.92.1 @@ -172,6 +172,11 @@ httpcore test + + com.google.errorprone + error_prone_annotations + test + diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/CopyWriter.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/CopyWriter.java index 4f9018bac0..47aa6b9f83 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/CopyWriter.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/CopyWriter.java @@ -16,11 +16,8 @@ package com.google.cloud.storage; -import static com.google.cloud.RetryHelper.runWithRetries; - import com.google.cloud.Restorable; import com.google.cloud.RestorableState; -import com.google.cloud.RetryHelper; import com.google.cloud.storage.spi.v1.StorageRpc; import com.google.cloud.storage.spi.v1.StorageRpc.RewriteRequest; import com.google.cloud.storage.spi.v1.StorageRpc.RewriteResponse; @@ -28,7 +25,7 @@ import java.io.Serializable; import java.util.Map; import java.util.Objects; -import java.util.concurrent.Callable; +import java.util.function.Function; /** * Google Storage blob copy writer. A {@code CopyWriter} object allows to copy both blob's data and @@ -100,21 +97,11 @@ public long getTotalBytesCopied() { */ public void copyChunk() { if (!isDone()) { - try { - this.rewriteResponse = - runWithRetries( - new Callable() { - @Override - public RewriteResponse call() { - return storageRpc.continueRewrite(rewriteResponse); - } - }, - serviceOptions.getRetrySettings(), - StorageImpl.EXCEPTION_HANDLER, - serviceOptions.getClock()); - } catch (RetryHelper.RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + this.rewriteResponse = + Retrying.run( + serviceOptions, + () -> storageRpc.continueRewrite(rewriteResponse), + Function.identity()); } } diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Retrying.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Retrying.java new file mode 100644 index 0000000000..3cae94ea71 --- /dev/null +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Retrying.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 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.storage; + +import static com.google.cloud.RetryHelper.runWithRetries; + +import com.google.api.core.ApiClock; +import com.google.api.gax.retrying.ResultRetryAlgorithm; +import com.google.api.gax.retrying.RetrySettings; +import com.google.cloud.BaseService; +import com.google.cloud.RetryHelper.RetryHelperException; +import java.util.concurrent.Callable; +import java.util.function.Function; + +final class Retrying { + + /** + * A convenience wrapper around {@link com.google.cloud.RetryHelper#runWithRetries(Callable, + * RetrySettings, ResultRetryAlgorithm, ApiClock)} that gives us centralized error translation and + * reduces some duplication in how we resolved the {@link RetrySettings} and {@link ApiClock}. + * + * @param options The {@link StorageOptions} which {@link RetrySettings} and {@link ApiClock} will + * be resolved from. + * @param c The {@link Callable} which will be passed to runWithRetries producing some {@code T}, + * can optionally return null + * @param f A post process mapping {@link Function} which can be used to transform the result from + * {@code c} if it is successful and non-null + * @param The result type of {@code c} + * @param The result type of any mapping that takes place via {@code f} + * @return A {@code U} (possibly null) after applying {@code f} to the result of {@code c} + * @throws StorageException if {@code c} fails due to any retry exhaustion + */ + static U run(StorageOptions options, Callable c, Function f) { + try { + T answer = + runWithRetries( + c, options.getRetrySettings(), BaseService.EXCEPTION_HANDLER, options.getClock()); + return answer == null ? null : f.apply(answer); + } catch (RetryHelperException e) { + throw StorageException.translateAndThrow(e); + } + } +} diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java index c3778a8175..2d7f9675c6 100644 --- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java +++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java @@ -16,8 +16,6 @@ package com.google.cloud.storage; -import static com.google.cloud.RetryHelper.runWithRetries; -import static com.google.cloud.storage.PolicyHelper.convertFromApiPolicy; import static com.google.cloud.storage.PolicyHelper.convertToApiPolicy; import static com.google.cloud.storage.SignedUrlEncodingHelper.Rfc3986UriEncode; import static com.google.cloud.storage.spi.v1.StorageRpc.Option.DELIMITER; @@ -38,7 +36,6 @@ import com.google.api.services.storage.model.BucketAccessControl; import com.google.api.services.storage.model.ObjectAccessControl; import com.google.api.services.storage.model.StorageObject; -import com.google.api.services.storage.model.TestIamPermissionsResponse; import com.google.auth.ServiceAccountSigner; import com.google.cloud.BaseService; import com.google.cloud.BatchResult; @@ -46,7 +43,6 @@ import com.google.cloud.PageImpl.NextPageFetcher; import com.google.cloud.Policy; import com.google.cloud.ReadChannel; -import com.google.cloud.RetryHelper.RetryHelperException; import com.google.cloud.Tuple; import com.google.cloud.WriteChannel; import com.google.cloud.storage.Acl.Entity; @@ -56,9 +52,7 @@ import com.google.cloud.storage.PostPolicyV4.PostFieldsV4; import com.google.cloud.storage.PostPolicyV4.PostPolicyV4Document; import com.google.cloud.storage.spi.v1.StorageRpc; -import com.google.cloud.storage.spi.v1.StorageRpc.RewriteResponse; import com.google.common.base.CharMatcher; -import com.google.common.base.Function; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; @@ -94,6 +88,7 @@ import java.util.TimeZone; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; +import java.util.function.Function; final class StorageImpl extends BaseService implements Storage { @@ -109,14 +104,6 @@ final class StorageImpl extends BaseService implements Storage { private static final int DEFAULT_BUFFER_SIZE = 15 * 1024 * 1024; private static final int MIN_BUFFER_SIZE = 256 * 1024; - private static final Function, Boolean> DELETE_FUNCTION = - new Function, Boolean>() { - @Override - public Boolean apply(Tuple tuple) { - return tuple.y(); - } - }; - private final StorageRpc storageRpc; StorageImpl(StorageOptions options) { @@ -128,22 +115,7 @@ public Boolean apply(Tuple tuple) { public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) { final com.google.api.services.storage.model.Bucket bucketPb = bucketInfo.toPb(); final Map optionsMap = optionMap(bucketInfo, options); - try { - return Bucket.fromPb( - this, - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.Bucket call() { - return storageRpc.create(bucketPb, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.create(bucketPb, optionsMap), (b) -> Bucket.fromPb(this, b)); } @Override @@ -211,23 +183,11 @@ private Blob internalCreate( Preconditions.checkNotNull(content); final StorageObject blobPb = info.toPb(); final Map optionsMap = optionMap(info, options); - try { - return Blob.fromPb( - this, - runWithRetries( - new Callable() { - @Override - public StorageObject call() { - return storageRpc.create( - blobPb, new ByteArrayInputStream(content, offset, length), optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> + storageRpc.create( + blobPb, new ByteArrayInputStream(content, offset, length), optionsMap), + (x) -> Blob.fromPb(this, x)); } @Override @@ -288,22 +248,7 @@ private static void uploadHelper(ReadableByteChannel reader, WriteChannel writer public Bucket get(String bucket, BucketGetOption... options) { final com.google.api.services.storage.model.Bucket bucketPb = BucketInfo.of(bucket).toPb(); final Map optionsMap = optionMap(options); - try { - com.google.api.services.storage.model.Bucket answer = - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.Bucket call() { - return storageRpc.get(bucketPb, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return answer == null ? null : Bucket.fromPb(this, answer); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.get(bucketPb, optionsMap), (b) -> Bucket.fromPb(this, b)); } @Override @@ -315,22 +260,7 @@ public Blob get(String bucket, String blob, BlobGetOption... options) { public Blob get(BlobId blob, BlobGetOption... options) { final StorageObject storedObject = blob.toPb(); final Map optionsMap = optionMap(blob, options); - try { - StorageObject storageObject = - runWithRetries( - new Callable() { - @Override - public StorageObject call() { - return storageRpc.get(storedObject, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return storageObject == null ? null : Blob.fromPb(this, storageObject); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.get(storedObject, optionsMap), (x) -> Blob.fromPb(this, x)); } @Override @@ -410,116 +340,53 @@ public Page list(final String bucket, BlobListOption... options) { private static Page listBuckets( final StorageOptions serviceOptions, final Map optionsMap) { - try { - Tuple> result = - runWithRetries( - new Callable< - Tuple>>() { - @Override - public Tuple> - call() { - return serviceOptions.getStorageRpcV1().list(optionsMap); - } - }, - serviceOptions.getRetrySettings(), - EXCEPTION_HANDLER, - serviceOptions.getClock()); - String cursor = result.x(); - Iterable buckets = - result.y() == null - ? ImmutableList.of() - : Iterables.transform( - result.y(), - new Function() { - @Override - public Bucket apply(com.google.api.services.storage.model.Bucket bucketPb) { - return Bucket.fromPb(serviceOptions.getService(), bucketPb); - } - }); - return new PageImpl<>( - new BucketPageFetcher(serviceOptions, cursor, optionsMap), cursor, buckets); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return Retrying.run( + serviceOptions, + () -> serviceOptions.getStorageRpcV1().list(optionsMap), + (result) -> { + String cursor = result.x(); + Iterable buckets = + result.y() == null + ? ImmutableList.of() + : Iterables.transform( + result.y(), bucketPb -> Bucket.fromPb(serviceOptions.getService(), bucketPb)); + return new PageImpl<>( + new BucketPageFetcher(serviceOptions, cursor, optionsMap), cursor, buckets); + }); } private static Page listBlobs( final String bucket, final StorageOptions serviceOptions, final Map optionsMap) { - try { - Tuple> result = - runWithRetries( - new Callable>>() { - @Override - public Tuple> call() { - return serviceOptions.getStorageRpcV1().list(bucket, optionsMap); - } - }, - serviceOptions.getRetrySettings(), - EXCEPTION_HANDLER, - serviceOptions.getClock()); - String cursor = result.x(); - Iterable blobs = - result.y() == null - ? ImmutableList.of() - : Iterables.transform( - result.y(), - new Function() { - @Override - public Blob apply(StorageObject storageObject) { - return Blob.fromPb(serviceOptions.getService(), storageObject); - } - }); - return new PageImpl<>( - new BlobPageFetcher(bucket, serviceOptions, cursor, optionsMap), cursor, blobs); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return Retrying.run( + serviceOptions, + () -> serviceOptions.getStorageRpcV1().list(bucket, optionsMap), + (result) -> { + String cursor = result.x(); + Iterable blobs = + result.y() == null + ? ImmutableList.of() + : Iterables.transform( + result.y(), + storageObject -> Blob.fromPb(serviceOptions.getService(), storageObject)); + return new PageImpl<>( + new BlobPageFetcher(bucket, serviceOptions, cursor, optionsMap), cursor, blobs); + }); } @Override public Bucket update(BucketInfo bucketInfo, BucketTargetOption... options) { final com.google.api.services.storage.model.Bucket bucketPb = bucketInfo.toPb(); final Map optionsMap = optionMap(bucketInfo, options); - try { - return Bucket.fromPb( - this, - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.Bucket call() { - return storageRpc.patch(bucketPb, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.patch(bucketPb, optionsMap), (x) -> Bucket.fromPb(this, x)); } @Override public Blob update(BlobInfo blobInfo, BlobTargetOption... options) { final StorageObject storageObject = blobInfo.toPb(); final Map optionsMap = optionMap(blobInfo, options); - try { - return Blob.fromPb( - this, - runWithRetries( - new Callable() { - @Override - public StorageObject call() { - return storageRpc.patch(storageObject, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.patch(storageObject, optionsMap), (x) -> Blob.fromPb(this, x)); } @Override @@ -531,20 +398,7 @@ public Blob update(BlobInfo blobInfo) { public boolean delete(String bucket, BucketSourceOption... options) { final com.google.api.services.storage.model.Bucket bucketPb = BucketInfo.of(bucket).toPb(); final Map optionsMap = optionMap(options); - try { - return runWithRetries( - new Callable() { - @Override - public Boolean call() { - return storageRpc.delete(bucketPb, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.delete(bucketPb, optionsMap), Function.identity()); } @Override @@ -556,20 +410,7 @@ public boolean delete(String bucket, String blob, BlobSourceOption... options) { public boolean delete(BlobId blob, BlobSourceOption... options) { final StorageObject storageObject = blob.toPb(); final Map optionsMap = optionMap(blob, options); - try { - return runWithRetries( - new Callable() { - @Override - public Boolean call() { - return storageRpc.delete(storageObject, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.delete(storageObject, optionsMap), Function.identity()); } @Override @@ -597,22 +438,8 @@ public Blob compose(final ComposeRequest composeRequest) { composeRequest.getTarget().getGeneration(), composeRequest.getTarget().getMetageneration(), composeRequest.getTargetOptions()); - try { - return Blob.fromPb( - this, - runWithRetries( - new Callable() { - @Override - public StorageObject call() { - return storageRpc.compose(sources, target, targetOptions); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> storageRpc.compose(sources, target, targetOptions), (x) -> Blob.fromPb(this, x)); } @Override @@ -627,29 +454,17 @@ public CopyWriter copy(final CopyRequest copyRequest) { copyRequest.getTarget().getGeneration(), copyRequest.getTarget().getMetageneration(), copyRequest.getTargetOptions()); - try { - RewriteResponse rewriteResponse = - runWithRetries( - new Callable() { - @Override - public RewriteResponse call() { - return storageRpc.openRewrite( - new StorageRpc.RewriteRequest( - source, - sourceOptions, - copyRequest.overrideInfo(), - targetObject, - targetOptions, - copyRequest.getMegabytesCopiedPerChunk())); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return new CopyWriter(getOptions(), rewriteResponse); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> + storageRpc.openRewrite( + new StorageRpc.RewriteRequest( + source, + sourceOptions, + copyRequest.overrideInfo(), + targetObject, + targetOptions, + copyRequest.getMegabytesCopiedPerChunk())), + (r) -> new CopyWriter(getOptions(), r)); } @Override @@ -661,20 +476,7 @@ public byte[] readAllBytes(String bucket, String blob, BlobSourceOption... optio public byte[] readAllBytes(BlobId blob, BlobSourceOption... options) { final StorageObject storageObject = blob.toPb(); final Map optionsMap = optionMap(blob, options); - try { - return runWithRetries( - new Callable() { - @Override - public byte[] call() { - return storageRpc.load(storageObject, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.load(storageObject, optionsMap), Function.identity()); } @Override @@ -1042,8 +844,7 @@ private SignatureInfo buildSignatureInfo( signatureInfoBuilder.setTimestamp(getOptions().getClock().millisTime()); - ImmutableMap.Builder extHeadersBuilder = - new ImmutableMap.Builder(); + ImmutableMap.Builder extHeadersBuilder = new ImmutableMap.Builder<>(); boolean isV4 = SignUrlOption.SignatureVersion.V4.equals( @@ -1065,16 +866,15 @@ private SignatureInfo buildSignatureInfo( (Map) optionMap.get(SignUrlOption.Option.EXT_HEADERS)); } - ImmutableMap.Builder queryParamsBuilder = - new ImmutableMap.Builder(); + ImmutableMap.Builder queryParamsBuilder = new ImmutableMap.Builder<>(); if (optionMap.containsKey(SignUrlOption.Option.QUERY_PARAMS)) { queryParamsBuilder.putAll( (Map) optionMap.get(SignUrlOption.Option.QUERY_PARAMS)); } return signatureInfoBuilder - .setCanonicalizedExtensionHeaders((Map) extHeadersBuilder.build()) - .setCanonicalizedQueryParams((Map) queryParamsBuilder.build()) + .setCanonicalizedExtensionHeaders(extHeadersBuilder.build()) + .setCanonicalizedQueryParams(queryParamsBuilder.build()) .build(); } @@ -1187,23 +987,8 @@ public void error(StorageException exception) { @Override public Acl getAcl(final String bucket, final Entity entity, BucketSourceOption... options) { - try { - final Map optionsMap = optionMap(options); - BucketAccessControl answer = - runWithRetries( - new Callable() { - @Override - public BucketAccessControl call() { - return storageRpc.getAcl(bucket, entity.toPb(), optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return answer == null ? null : Acl.fromPb(answer); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + final Map optionsMap = optionMap(options); + return run(() -> storageRpc.getAcl(bucket, entity.toPb(), optionsMap), Acl::fromPb); } @Override @@ -1214,21 +999,8 @@ public Acl getAcl(final String bucket, final Entity entity) { @Override public boolean deleteAcl( final String bucket, final Entity entity, BucketSourceOption... options) { - try { - final Map optionsMap = optionMap(options); - return runWithRetries( - new Callable() { - @Override - public Boolean call() { - return storageRpc.deleteAcl(bucket, entity.toPb(), optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + final Map optionsMap = optionMap(options); + return run(() -> storageRpc.deleteAcl(bucket, entity.toPb(), optionsMap), Function.identity()); } @Override @@ -1239,22 +1011,8 @@ public boolean deleteAcl(final String bucket, final Entity entity) { @Override public Acl createAcl(String bucket, Acl acl, BucketSourceOption... options) { final BucketAccessControl aclPb = acl.toBucketPb().setBucket(bucket); - try { - final Map optionsMap = optionMap(options); - return Acl.fromPb( - runWithRetries( - new Callable() { - @Override - public BucketAccessControl call() { - return storageRpc.createAcl(aclPb, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + final Map optionsMap = optionMap(options); + return run(() -> storageRpc.createAcl(aclPb, optionsMap), Acl::fromPb); } @Override @@ -1265,22 +1023,8 @@ public Acl createAcl(String bucket, Acl acl) { @Override public Acl updateAcl(String bucket, Acl acl, BucketSourceOption... options) { final BucketAccessControl aclPb = acl.toBucketPb().setBucket(bucket); - try { - final Map optionsMap = optionMap(options); - return Acl.fromPb( - runWithRetries( - new Callable() { - @Override - public BucketAccessControl call() { - return storageRpc.patchAcl(aclPb, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + final Map optionsMap = optionMap(options); + return run(() -> storageRpc.patchAcl(aclPb, optionsMap), Acl::fromPb); } @Override @@ -1290,23 +1034,13 @@ public Acl updateAcl(String bucket, Acl acl) { @Override public List listAcls(final String bucket, BucketSourceOption... options) { - try { - final Map optionsMap = optionMap(options); - List answer = - runWithRetries( - new Callable>() { - @Override - public List call() { - return storageRpc.listAcls(bucket, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return Lists.transform(answer, Acl.FROM_BUCKET_PB_FUNCTION); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + final Map optionsMap = optionMap(options); + return run( + () -> storageRpc.listAcls(bucket, optionsMap), + (answer) -> + answer.stream() + .map(Acl.FROM_BUCKET_PB_FUNCTION) + .collect(ImmutableList.toImmutableList())); } @Override @@ -1316,140 +1050,52 @@ public List listAcls(final String bucket) { @Override public Acl getDefaultAcl(final String bucket, final Entity entity) { - try { - ObjectAccessControl answer = - runWithRetries( - new Callable() { - @Override - public ObjectAccessControl call() { - return storageRpc.getDefaultAcl(bucket, entity.toPb()); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return answer == null ? null : Acl.fromPb(answer); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.getDefaultAcl(bucket, entity.toPb()), Acl::fromPb); } @Override public boolean deleteDefaultAcl(final String bucket, final Entity entity) { - try { - return runWithRetries( - new Callable() { - @Override - public Boolean call() { - return storageRpc.deleteDefaultAcl(bucket, entity.toPb()); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.deleteDefaultAcl(bucket, entity.toPb()), Function.identity()); } @Override public Acl createDefaultAcl(String bucket, Acl acl) { final ObjectAccessControl aclPb = acl.toObjectPb().setBucket(bucket); - try { - return Acl.fromPb( - runWithRetries( - new Callable() { - @Override - public ObjectAccessControl call() { - return storageRpc.createDefaultAcl(aclPb); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.createDefaultAcl(aclPb), Acl::fromPb); } @Override public Acl updateDefaultAcl(String bucket, Acl acl) { final ObjectAccessControl aclPb = acl.toObjectPb().setBucket(bucket); - try { - return Acl.fromPb( - runWithRetries( - new Callable() { - @Override - public ObjectAccessControl call() { - return storageRpc.patchDefaultAcl(aclPb); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.patchDefaultAcl(aclPb), Acl::fromPb); } @Override public List listDefaultAcls(final String bucket) { - try { - List answer = - runWithRetries( - new Callable>() { - @Override - public List call() { - return storageRpc.listDefaultAcls(bucket); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return Lists.transform(answer, Acl.FROM_OBJECT_PB_FUNCTION); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> storageRpc.listDefaultAcls(bucket), + (answer) -> + answer.stream() + .map(Acl.FROM_OBJECT_PB_FUNCTION) + .collect(ImmutableList.toImmutableList())); } @Override public Acl getAcl(final BlobId blob, final Entity entity) { - try { - ObjectAccessControl answer = - runWithRetries( - new Callable() { - @Override - public ObjectAccessControl call() { - return storageRpc.getAcl( - blob.getBucket(), blob.getName(), blob.getGeneration(), entity.toPb()); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return answer == null ? null : Acl.fromPb(answer); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> + storageRpc.getAcl( + blob.getBucket(), blob.getName(), blob.getGeneration(), entity.toPb()), + Acl::fromPb); } @Override public boolean deleteAcl(final BlobId blob, final Entity entity) { - try { - return runWithRetries( - new Callable() { - @Override - public Boolean call() { - return storageRpc.deleteAcl( - blob.getBucket(), blob.getName(), blob.getGeneration(), entity.toPb()); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> + storageRpc.deleteAcl( + blob.getBucket(), blob.getName(), blob.getGeneration(), entity.toPb()), + Function.identity()); } @Override @@ -1459,21 +1105,7 @@ public Acl createAcl(final BlobId blob, final Acl acl) { .setBucket(blob.getBucket()) .setObject(blob.getName()) .setGeneration(blob.getGeneration()); - try { - return Acl.fromPb( - runWithRetries( - new Callable() { - @Override - public ObjectAccessControl call() { - return storageRpc.createAcl(aclPb); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.createAcl(aclPb), Acl::fromPb); } @Override @@ -1483,61 +1115,24 @@ public Acl updateAcl(BlobId blob, Acl acl) { .setBucket(blob.getBucket()) .setObject(blob.getName()) .setGeneration(blob.getGeneration()); - try { - return Acl.fromPb( - runWithRetries( - new Callable() { - @Override - public ObjectAccessControl call() { - return storageRpc.patchAcl(aclPb); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.patchAcl(aclPb), Acl::fromPb); } @Override public List listAcls(final BlobId blob) { - try { - List answer = - runWithRetries( - new Callable>() { - @Override - public List call() { - return storageRpc.listAcls( - blob.getBucket(), blob.getName(), blob.getGeneration()); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return Lists.transform(answer, Acl.FROM_OBJECT_PB_FUNCTION); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> storageRpc.listAcls(blob.getBucket(), blob.getName(), blob.getGeneration()), + (answer) -> + answer.stream() + .map(Acl.FROM_OBJECT_PB_FUNCTION) + .collect(ImmutableList.toImmutableList())); } public HmacKey createHmacKey( final ServiceAccount serviceAccount, final CreateHmacKeyOption... options) { - try { - return HmacKey.fromPb( - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.HmacKey call() { - return storageRpc.createHmacKey(serviceAccount.getEmail(), optionMap(options)); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> storageRpc.createHmacKey(serviceAccount.getEmail(), optionMap(options)), + HmacKey::fromPb); } @Override @@ -1547,40 +1142,14 @@ public Page listHmacKeys(ListHmacKeysOption... options) { @Override public HmacKeyMetadata getHmacKey(final String accessId, final GetHmacKeyOption... options) { - try { - return HmacKeyMetadata.fromPb( - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.HmacKeyMetadata call() { - return storageRpc.getHmacKey(accessId, optionMap(options)); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.getHmacKey(accessId, optionMap(options)), HmacKeyMetadata::fromPb); } private HmacKeyMetadata updateHmacKey( final HmacKeyMetadata hmacKeyMetadata, final UpdateHmacKeyOption... options) { - try { - return HmacKeyMetadata.fromPb( - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.HmacKeyMetadata call() { - return storageRpc.updateHmacKey(hmacKeyMetadata.toPb(), optionMap(options)); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> storageRpc.updateHmacKey(hmacKeyMetadata.toPb(), optionMap(options)), + HmacKeyMetadata::fromPb); } @Override @@ -1599,176 +1168,79 @@ public HmacKeyMetadata updateHmacKeyState( @Override public void deleteHmacKey(final HmacKeyMetadata metadata, final DeleteHmacKeyOption... options) { - try { - runWithRetries( - new Callable() { - @Override - public Void call() { + run( + (Callable) + () -> { storageRpc.deleteHmacKey(metadata.toPb(), optionMap(options)); return null; - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + }, + Function.identity()); } private static Page listHmacKeys( final StorageOptions serviceOptions, final Map options) { - try { - Tuple> result = - runWithRetries( - new Callable< - Tuple< - String, Iterable>>() { - @Override - public Tuple< - String, Iterable> - call() { - return serviceOptions.getStorageRpcV1().listHmacKeys(options); - } - }, - serviceOptions.getRetrySettings(), - EXCEPTION_HANDLER, - serviceOptions.getClock()); - String cursor = result.x(); - final Iterable metadata = - result.y() == null - ? ImmutableList.of() - : Iterables.transform( - result.y(), - new Function< - com.google.api.services.storage.model.HmacKeyMetadata, HmacKeyMetadata>() { - @Override - public HmacKeyMetadata apply( - com.google.api.services.storage.model.HmacKeyMetadata metadataPb) { - return HmacKeyMetadata.fromPb(metadataPb); - } - }); - return new PageImpl<>( - new HmacKeyMetadataPageFetcher(serviceOptions, options), cursor, metadata); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return Retrying.run( + serviceOptions, + () -> serviceOptions.getStorageRpcV1().listHmacKeys(options), + (result) -> { + String cursor = result.x(); + final Iterable metadata = + result.y() == null + ? ImmutableList.of() + : Iterables.transform(result.y(), HmacKeyMetadata::fromPb); + return new PageImpl<>( + new HmacKeyMetadataPageFetcher(serviceOptions, options), cursor, metadata); + }); } @Override public Policy getIamPolicy(final String bucket, BucketSourceOption... options) { - try { - final Map optionsMap = optionMap(options); - return convertFromApiPolicy( - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.Policy call() { - return storageRpc.getIamPolicy(bucket, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + final Map optionsMap = optionMap(options); + return run( + () -> storageRpc.getIamPolicy(bucket, optionsMap), PolicyHelper::convertFromApiPolicy); } @Override public Policy setIamPolicy( final String bucket, final Policy policy, BucketSourceOption... options) { - try { - final Map optionsMap = optionMap(options); - return convertFromApiPolicy( - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.Policy call() { - return storageRpc.setIamPolicy(bucket, convertToApiPolicy(policy), optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + final Map optionsMap = optionMap(options); + return run( + () -> storageRpc.setIamPolicy(bucket, convertToApiPolicy(policy), optionsMap), + PolicyHelper::convertFromApiPolicy); } @Override public List testIamPermissions( final String bucket, final List permissions, BucketSourceOption... options) { - try { - final Map optionsMap = optionMap(options); - TestIamPermissionsResponse response = - runWithRetries( - new Callable() { - @Override - public TestIamPermissionsResponse call() { - return storageRpc.testIamPermissions(bucket, permissions, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - final Set heldPermissions = - response.getPermissions() != null - ? ImmutableSet.copyOf(response.getPermissions()) - : ImmutableSet.of(); - return Lists.transform( - permissions, - new Function() { - @Override - public Boolean apply(String permission) { - return heldPermissions.contains(permission); - } - }); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + final Map optionsMap = optionMap(options); + return run( + () -> storageRpc.testIamPermissions(bucket, permissions, optionsMap), + (response) -> { + final Set heldPermissions = + response.getPermissions() != null + ? ImmutableSet.copyOf(response.getPermissions()) + : ImmutableSet.of(); + return permissions.stream() + .map(heldPermissions::contains) + .collect(ImmutableList.toImmutableList()); + }); } @Override public Bucket lockRetentionPolicy(BucketInfo bucketInfo, BucketTargetOption... options) { final com.google.api.services.storage.model.Bucket bucketPb = bucketInfo.toPb(); final Map optionsMap = optionMap(bucketInfo, options); - try { - return Bucket.fromPb( - this, - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.Bucket call() { - return storageRpc.lockRetentionPolicy(bucketPb, optionsMap); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock())); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run( + () -> storageRpc.lockRetentionPolicy(bucketPb, optionsMap), (x) -> Bucket.fromPb(this, x)); } @Override public ServiceAccount getServiceAccount(final String projectId) { - try { - com.google.api.services.storage.model.ServiceAccount answer = - runWithRetries( - new Callable() { - @Override - public com.google.api.services.storage.model.ServiceAccount call() { - return storageRpc.getServiceAccount(projectId); - } - }, - getOptions().getRetrySettings(), - EXCEPTION_HANDLER, - getOptions().getClock()); - return answer == null ? null : ServiceAccount.fromPb(answer); - } catch (RetryHelperException e) { - throw StorageException.translateAndThrow(e); - } + return run(() -> storageRpc.getServiceAccount(projectId), ServiceAccount::fromPb); + } + + private U run(Callable c, Function f) { + return Retrying.run(getOptions(), c, f); } private static void addToOptionMap( diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/PackagePrivateMethodWorkarounds.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/PackagePrivateMethodWorkarounds.java new file mode 100644 index 0000000000..0706df1a7b --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/PackagePrivateMethodWorkarounds.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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.storage; + +import com.google.cloud.storage.BucketInfo.BuilderImpl; + +/** + * Several classes in the High Level Model for storage include package-local constructors and + * methods. For conformance testing we don't want to exist in the com.google.cloud.storage package + * to ensure we're interacting with the public api, however in a few select cases we need to change + * the instance of {@link Storage} which an object holds on to. The utilities in this class allow us + * to perform these operations. + */ +public final class PackagePrivateMethodWorkarounds { + + private PackagePrivateMethodWorkarounds() {} + + public static Bucket bucketCopyWithStorage(Bucket b, Storage s) { + BucketInfo.BuilderImpl builder = (BuilderImpl) BucketInfo.fromPb(b.toPb()).toBuilder(); + return new Bucket(s, builder); + } + + public static Blob blobCopyWithStorage(Blob b, Storage s) { + BlobInfo.BuilderImpl builder = (BlobInfo.BuilderImpl) BlobInfo.fromPb(b.toPb()).toBuilder(); + return new Blob(s, builder); + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/CleanupStrategy.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/CleanupStrategy.java new file mode 100644 index 0000000000..473e3cb4f2 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/CleanupStrategy.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +enum CleanupStrategy { + ALWAYS, + ONLY_ON_SUCCESS, + NEVER +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/Ctx.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/Ctx.java new file mode 100644 index 0000000000..fa24be23c4 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/Ctx.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.conformance.retry.Functions.EConsumer; +import com.google.cloud.storage.conformance.retry.Functions.EFunction; +import com.google.errorprone.annotations.Immutable; + +/** + * A simple context object used to track an instance of {@link Storage} along with {@link State} and + * provide some convenience methods for creating new instances. + */ +@Immutable +final class Ctx { + + private final Storage storage; + private final State state; + + private Ctx(Storage s, State t) { + this.storage = s; + this.state = t; + } + + /** Create a new instance of {@link Ctx} */ + static Ctx ctx(Storage storage, State state) { + return new Ctx(storage, state); + } + + public Storage getStorage() { + return storage; + } + + public State getState() { + return state; + } + + /** + * Create a new instance of {@link Ctx} by first applying {@code f} to {@code this.storage}. + * {@code this.state} is passed along unchanged. + */ + public Ctx leftMap(EFunction f) throws Throwable { + return new Ctx(f.apply(storage), state); + } + + /** + * Create a new instance of {@link Ctx} by first applying {@code f} to {@code this.state}. {@code + * this.storage} is passed along unchanged. + */ + public Ctx map(EFunction f) throws Throwable { + return new Ctx(storage, f.apply(state)); + } + + /** + * Apply {@code f} by providing {@code this.state}. + * + *

This method is provided as convenience for those methods which have void return. In general + * {@link Ctx#map(EFunction)} should be used. + */ + public Ctx peek(EConsumer f) throws Throwable { + f.consume(state); + return this; + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/CtxFunctions.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/CtxFunctions.java new file mode 100644 index 0000000000..75f56c8527 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/CtxFunctions.java @@ -0,0 +1,146 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static com.google.common.collect.Sets.newHashSet; + +import com.google.cloud.conformance.storage.v1.Resource; +import com.google.cloud.storage.Acl; +import com.google.cloud.storage.Acl.Role; +import com.google.cloud.storage.Acl.User; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.ServiceAccount; +import com.google.cloud.storage.conformance.retry.Functions.CtxFunction; +import com.google.common.base.Joiner; +import java.util.HashSet; + +/** + * Define a set of {@link CtxFunction} which are used in mappings as well as general setup/tear down + * of specific tests. + * + *

Functions are grouped into nested classes which try to hint at the area they operate within. + * Client side-only, or performing an RPC, setup or tear down and so on. + * + * @see RpcMethodMapping + * @see RpcMethodMapping.Builder + * @see RpcMethodMappings + */ +final class CtxFunctions { + + private static final class Util { + private static final CtxFunction blobIdAndBlobInfo = + (ctx, c) -> ctx.map(state -> state.with(BlobInfo.newBuilder(state.getBlobId()).build())); + } + + static final class Local { + static final CtxFunction blobCopy = + (ctx, c) -> ctx.map(s -> s.withCopyDest(BlobId.of(c.getBucketName2(), c.getObjectName()))); + + static final CtxFunction bucketInfo = + (ctx, c) -> ctx.map(s -> s.with(BucketInfo.of(c.getBucketName()))); + static final CtxFunction blobIdWithoutGeneration = + (ctx, c) -> ctx.map(s -> s.with(BlobId.of(c.getBucketName(), c.getObjectName()))); + static final CtxFunction blobIdWithGenerationZero = + (ctx, c) -> ctx.map(s -> s.with(BlobId.of(c.getBucketName(), c.getObjectName(), 0L))); + static final CtxFunction blobInfoWithoutGeneration = + blobIdWithoutGeneration.andThen(Util.blobIdAndBlobInfo); + static final CtxFunction blobInfoWithGenerationZero = + blobIdWithGenerationZero.andThen(Util.blobIdAndBlobInfo); + } + + static final class Rpc { + static final CtxFunction bucket = + (ctx, c) -> + ctx.map(state -> state.with(ctx.getStorage().get(state.getBucketInfo().getName()))); + static final CtxFunction blobWithGeneration = + (ctx, c) -> ctx.map(state -> state.with(ctx.getStorage().get(state.getBlobId()))); + static final CtxFunction createEmptyBlob = + (ctx, c) -> ctx.map(state -> state.with(ctx.getStorage().create(state.getBlobInfo()))); + } + + static final class ResourceSetup { + private static final CtxFunction bucket = + (ctx, c) -> { + BucketInfo bucketInfo = BucketInfo.newBuilder(c.getBucketName()).build(); + Bucket resolvedBucket = ctx.getStorage().create(bucketInfo); + return ctx.map(s -> s.with(resolvedBucket)); + }; + private static final CtxFunction object = + (ctx, c) -> { + BlobInfo blobInfo = + BlobInfo.newBuilder(ctx.getState().getBucket().getName(), c.getObjectName()).build(); + Blob resolvedBlob = ctx.getStorage().create(blobInfo); + return ctx.map(s -> s.with(resolvedBlob)); + }; + private static final CtxFunction serviceAccount = + (ctx, c) -> + ctx.map(s -> s.with(ServiceAccount.of(c.getServiceAccountSigner().getAccount()))); + private static final CtxFunction hmacKey = + (ctx, c) -> ctx.map(s -> s.with(ctx.getStorage().createHmacKey(s.getServiceAccount()))); + + private static final CtxFunction processResources = + (ctx, c) -> { + HashSet resources = newHashSet(c.getMethod().getResourcesList()); + CtxFunction f = CtxFunction.identity(); + if (resources.contains(Resource.BUCKET)) { + f = f.andThen(ResourceSetup.bucket); + resources.remove(Resource.BUCKET); + } + + if (resources.contains(Resource.OBJECT)) { + f = f.andThen(ResourceSetup.object); + resources.remove(Resource.OBJECT); + } + + if (resources.contains(Resource.HMAC_KEY)) { + f = f.andThen(serviceAccount).andThen(hmacKey); + resources.remove(Resource.HMAC_KEY); + } + + if (!resources.isEmpty()) { + throw new IllegalStateException( + String.format("Unhandled Method Resource [%s]", Joiner.on(", "))); + } + + return f.apply(ctx, c); + }; + + private static final CtxFunction allUsersReaderAcl = + (ctx, c) -> ctx.map(s -> s.with(Acl.of(User.ofAllUsers(), Role.READER))); + + static final CtxFunction defaultSetup = processResources.andThen(allUsersReaderAcl); + } + + static final class ResourceTeardown { + static final CtxFunction object = + (ctx, c) -> { + BlobInfo blobInfo = + BlobInfo.newBuilder(ctx.getState().getBucket().getName(), c.getObjectName()).build(); + ctx.getStorage().delete(blobInfo.getBlobId()); + return ctx.map(s -> s.with((Blob) null)); + }; + static final CtxFunction bucket = + (ctx, c) -> { + ctx.getState().getBucket().delete(); + return ctx.map(s -> s.with((Bucket) null)); + }; + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/Functions.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/Functions.java new file mode 100644 index 0000000000..cda0eeddef --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/Functions.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +/** + * A set of Functional interface types which are used in Retry Conformance tests. + * + *

All functions allow checked exceptions to be thrown, whereas their siblings in {@code + * java.util.function} do not. + */ +final class Functions { + + /** + * A specialized BiFunction which cuts down on boilerplate and provides an {@link + * CtxFunction#andThen(CtxFunction) andThen} which carries through the BiFunction-ness. + */ + @FunctionalInterface + interface CtxFunction { + + Ctx apply(Ctx ctx, TestRetryConformance trc) throws Throwable; + + default CtxFunction andThen(CtxFunction f) { + return (Ctx ctx, TestRetryConformance trc) -> f.apply(apply(ctx, trc), trc); + } + + static CtxFunction identity() { + return (ctx, c) -> ctx; + } + } + + /** + * Define a Function which can throw, this simplifies the code where a checked exception is + * declared. These Functions only exist in the context of tests so if a throw happens it will be + * handled at a per-test level. + */ + @FunctionalInterface + interface EFunction { + B apply(A a) throws Throwable; + } + + /** + * Define a Consumer which can throw, this simplifies the code where a checked exception is + * declared. These Consumers only exist in the context of tests so if a throw happens it will be + * handled at a per-test level. + */ + @FunctionalInterface + interface EConsumer { + void consume(A a) throws Throwable; + } + + /** + * Define a function which has a void return. This is definition is absolutely not pure and only + * exists because some methods on the public api for storage have void return type. + */ + @FunctionalInterface + interface VoidFunction { + void apply() throws Throwable; + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/GracefulConformanceEnforcement.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/GracefulConformanceEnforcement.java new file mode 100644 index 0000000000..2fd4f90bb1 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/GracefulConformanceEnforcement.java @@ -0,0 +1,107 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static org.junit.Assert.assertNotNull; + +import com.google.common.collect.ImmutableSet; +import com.google.common.io.CharStreams; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Set; +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * As the adherence of {@link com.google.cloud.storage.Storage} to the retry conformance test suite + * is an ongoing effort, we need a way in which those tests which are not yet in compliance do not + * server as blockers for other features and commits. + * + *

This class provides a transparent means of enforcing the reporting of failed tests when ran in + * a CI environment. When a test is run, if it fails for any reason the test name will be checked + * against a list of known complying tests. If the tests name is missing from the known list, then + * the failure will be wrapped in an assumption failure to show up as a skipped test rather than a + * failed one. + */ +final class GracefulConformanceEnforcement implements TestRule { + + private final String testName; + private final Set testNamesWhichShouldSucceed; + + public GracefulConformanceEnforcement(String testName) { + this.testName = testName; + this.testNamesWhichShouldSucceed = loadTestNamesWhichShouldSucceed(); + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + base.evaluate(); + } catch (AssumptionViolatedException e) { + // pass through any assumption/ignore errors as they are + throw e; + } catch (Throwable t) { + if (testNamesWhichShouldSucceed.contains(testName)) { + throw t; + } else { + if (isRunningInCI()) { + throw new AssumptionViolatedException( + String.format( + "Test %s is not expected to succeed yet, downgrading failure to ignored.", + testName), + t); + } else { + throw t; + } + } + } + } + }; + } + + private static boolean isRunningInCI() { + return "test".equals(System.getenv("JOB_TYPE")) + || "integration".equals(System.getenv("JOB_TYPE")); + } + + private static Set loadTestNamesWhichShouldSucceed() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + InputStream inputStream = + cl.getResourceAsStream( + "com/google/cloud/storage/conformance/retry/testNamesWhichShouldSucceed.txt"); + assertNotNull(inputStream); + try { + return CharStreams.readLines(new InputStreamReader(inputStream)).stream() + .map(String::trim) + .filter(s -> !s.isEmpty() && !s.startsWith("#")) + .collect(ImmutableSet.toImmutableSet()); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + try { + inputStream.close(); + } catch (IOException ignore) { + } + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ITRetryConformanceTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ITRetryConformanceTest.java new file mode 100644 index 0000000000..73f15112b5 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ITRetryConformanceTest.java @@ -0,0 +1,441 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static com.google.cloud.storage.PackagePrivateMethodWorkarounds.blobCopyWithStorage; +import static com.google.cloud.storage.PackagePrivateMethodWorkarounds.bucketCopyWithStorage; +import static com.google.cloud.storage.conformance.retry.Ctx.ctx; +import static com.google.cloud.storage.conformance.retry.State.empty; +import static java.util.Objects.requireNonNull; +import static org.junit.Assert.assertNotNull; + +import com.google.cloud.conformance.storage.v1.InstructionList; +import com.google.cloud.conformance.storage.v1.Method; +import com.google.cloud.conformance.storage.v1.RetryTest; +import com.google.cloud.conformance.storage.v1.RetryTests; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.conformance.retry.Functions.CtxFunction; +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.util.JsonFormat; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.junit.AssumptionViolatedException; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized.Parameters; + +/** + * Load and dynamically generate a series of test cases to verify if the {@link Storage} and + * associated high level classes adhere to expected retry behavior. + * + *

This class dynamically generates test cases based on resources from the + * google-cloud-conformance-tests artifact and a set of defined mappings from {@link + * RpcMethodMappings}. + */ +@RunWith(ParallelParameterized.class) +public class ITRetryConformanceTest { + private static final Logger LOGGER = Logger.getLogger(ITRetryConformanceTest.class.getName()); + + @ClassRule public static final TestBench TEST_BENCH = TestBench.newBuilder().build(); + + @Rule(order = 1) + public final GracefulConformanceEnforcement gracefulConformanceEnforcement; + + @Rule(order = 2) + public final RetryTestFixture retryTestFixture; + + private final TestRetryConformance testRetryConformance; + private final RpcMethodMapping mapping; + + public ITRetryConformanceTest( + TestRetryConformance testRetryConformance, RpcMethodMapping mapping) { + this.testRetryConformance = testRetryConformance; + this.mapping = mapping; + this.gracefulConformanceEnforcement = + new GracefulConformanceEnforcement(testRetryConformance.getTestName()); + this.retryTestFixture = + new RetryTestFixture(CleanupStrategy.ALWAYS, TEST_BENCH, testRetryConformance); + } + + /** + * Run an individual test case. 1. Create two storage clients, one for setup/teardown and one for + * test execution 2. Run setup 3. Run test 4. Run teardown + */ + @Test + public void test() throws Throwable { + Storage nonTestStorage = retryTestFixture.getNonTestStorage(); + Storage testStorage = retryTestFixture.getTestStorage(); + + Ctx ctx = ctx(nonTestStorage, empty()); + + LOGGER.fine("Running setup..."); + Ctx postSetupCtx = + mapping.getSetup().apply(ctx, testRetryConformance).leftMap(s -> testStorage); + LOGGER.fine("Running setup complete"); + + LOGGER.fine("Running test..."); + Ctx postTestCtx = + getReplaceStorageInObjectsFromCtx() + .andThen(mapping.getTest()) + .apply(postSetupCtx, testRetryConformance) + .leftMap(s -> nonTestStorage); + LOGGER.fine("Running test complete"); + + LOGGER.fine("Running teardown..."); + getReplaceStorageInObjectsFromCtx() + .andThen(mapping.getTearDown()) + .apply(postTestCtx, testRetryConformance); + LOGGER.fine("Running teardown complete"); + } + + /** + * Load all of the tests and return a {@code Collection} representing the set of tests. + * Each entry in the returned collection is the set of parameters to the constructor of this test + * class. + * + *

The results of this method will then be run by JUnit's Parameterized test runner + */ + @Parameters(name = "{0}") + public static Collection testCases() throws IOException { + RetryTestCaseResolver resolver = + RetryTestCaseResolver.newBuilder() + .setRetryTestsJsonResourcePath( + "com/google/cloud/conformance/storage/v1/retry_tests.json") + .setMappings(new RpcMethodMappings()) + .setHost(TEST_BENCH.getBaseUri().replaceAll("https?://", "")) + .setTestAllowFilter(RetryTestCaseResolver.includeAll()) + .build(); + + return resolver.getRetryTestCases().stream() + .map(rtc -> new Object[] {rtc.testRetryConformance, rtc.rpcMethodMapping}) + .collect(ImmutableList.toImmutableList()); + } + + /** + * When a "higher level object" ({@link com.google.cloud.storage.Bucket}, {@link + * com.google.cloud.storage.Blob}, etc.) is created as part of setup it keeps a reference to the + * instance of {@link Storage} used to create it. When we run our tests we need the instance of + * {@link Storage} to be the instance with the headers to signal the retry test. + * + *

The function returned will inspect the {@link State} and create copies of any "higher level + * objects" which are present replacing the instance of {@link Storage} from the provided ctx. + */ + private static CtxFunction getReplaceStorageInObjectsFromCtx() { + return (ctx, c) -> { + State s = ctx.getState(); + if (s.hasBucket()) { + s = s.with(bucketCopyWithStorage(s.getBucket(), ctx.getStorage())); + } + if (s.hasBlob()) { + s = s.with(blobCopyWithStorage(s.getBlob(), ctx.getStorage())); + } + final State state = s; + return ctx.map(x -> state); + }; + } + + /** + * Helper class which encapsulates all the logic necessary to resolve and crete a test case for + * each defined scenario from google-cloud-conformance-tests and our defined {@link + * RpcMethodMappings}. + */ + private static final class RetryTestCaseResolver { + private static final String HEX_SHUFFLE_SEED_OVERRIDE = + System.getProperty("HEX_SHUFFLE_SEED_OVERRIDE"); + + private final String retryTestsJsonResourcePath; + private final RpcMethodMappings mappings; + private final BiPredicate testAllowFilter; + private final Random rand; + private final String host; + + RetryTestCaseResolver( + String retryTestsJsonResourcePath, + RpcMethodMappings mappings, + BiPredicate testAllowFilter, + Random rand, + String host) { + this.retryTestsJsonResourcePath = retryTestsJsonResourcePath; + this.mappings = mappings; + this.testAllowFilter = testAllowFilter; + this.rand = rand; + this.host = host; + } + + /** Load, permute and generate all RetryTestCases which are to be run in this suite */ + List getRetryTestCases() throws IOException { + RetryTests retryTests = loadRetryTestsDefinition(); + + // sort the defined RetryTest by id, so we have a stable ordering while generating cases. + List retryTestCases = + retryTests.getRetryTestsList().stream() + .sorted(Comparator.comparingInt(RetryTest::getId)) + .collect(Collectors.toList()); + + List testCases = generateTestCases(mappings, retryTestCases); + + // Shuffle our test cases to ensure we don't have any between case ordering weirdness + Collections.shuffle(testCases, rand); + + validateGeneratedTestCases(mappings, testCases); + + return testCases; + } + + /** Load the defined scenarios from google-cloud-conformance-tests */ + private RetryTests loadRetryTestsDefinition() throws IOException { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + InputStream dataJson = cl.getResourceAsStream(retryTestsJsonResourcePath); + assertNotNull( + String.format("Unable to load test definition: %s", retryTestsJsonResourcePath), + dataJson); + + InputStreamReader reader = new InputStreamReader(dataJson, Charsets.UTF_8); + RetryTests.Builder testBuilder = RetryTests.newBuilder(); + JsonFormat.parser().merge(reader, testBuilder); + return testBuilder.build(); + } + + /** Permute the RetryTest, Instructions and methods with our mappings */ + private List generateTestCases( + RpcMethodMappings rpcMethodMappings, List retryTests) { + + List testCases = new ArrayList<>(); + for (RetryTest testCase : retryTests) { + for (InstructionList instructionList : testCase.getCasesList()) { + for (Method method : testCase.getMethodsList()) { + String methodName = method.getName(); + RpcMethod key = RpcMethod.storage.lookup.get(methodName); + assertNotNull( + String.format("Unable to resolve RpcMethod for value '%s'", methodName), key); + // get all RpcMethodMappings which are defined for key + List mappings = + rpcMethodMappings.get(key).stream() + .sorted(Comparator.comparingInt(RpcMethodMapping::getMappingId)) + .collect(Collectors.toList()); + // if we don't have any mappings defined for the provide key, generate a case that when + // run reports an ignored test. This is done for the sake of completeness and to be + // aware of a lack of mapping. + if (mappings.isEmpty()) { + TestRetryConformance testRetryConformance = + new TestRetryConformance( + host, + testCase.getId(), + method, + instructionList, + testCase.getPreconditionProvided(), + false); + if (testAllowFilter.test(key, testRetryConformance)) { + testCases.add( + new RetryTestCase(testRetryConformance, RpcMethodMapping.notImplemented(key))); + } + } else { + for (RpcMethodMapping mapping : mappings) { + TestRetryConformance testRetryConformance = + new TestRetryConformance( + host, + testCase.getId(), + method, + instructionList, + testCase.getPreconditionProvided(), + testCase.getExpectSuccess(), + mapping.getMappingId()); + // check that this case is allowed based on the provided filter + if (testAllowFilter.test(key, testRetryConformance)) { + // check that the defined mapping is applicable to the case we've resolved. + // Many mappings are conditionally valid and depend on the defined case. + if (mapping.getApplicable().test(testRetryConformance)) { + testCases.add(new RetryTestCase(testRetryConformance, mapping)); + } else { + // when the mapping is determined to not be applicable to this case, generate + // a synthetic mapping which will report as an ignored test. This is done for + // the sake of completeness. + RpcMethodMapping build = + mapping + .toBuilder() + .withSetup(CtxFunction.identity()) + .withTest( + (s, c) -> { + throw new AssumptionViolatedException( + "applicability predicate evaluated to false"); + }) + .withTearDown(CtxFunction.identity()) + .build(); + testCases.add(new RetryTestCase(testRetryConformance, build)); + } + } + } + } + } + } + } + return testCases; + } + + private void validateGeneratedTestCases( + RpcMethodMappings rpcMethodMappings, List data) { + Set unusedMappings = + rpcMethodMappings.differenceMappingIds( + data.stream() + .map(rtc -> rtc.testRetryConformance.getMappingId()) + .collect(Collectors.toSet())); + + if (!unusedMappings.isEmpty()) { + LOGGER.warning( + String.format( + "Declared but unused mappings with ids: [%s]", + Joiner.on(", ").join(unusedMappings))); + } + } + + static Builder newBuilder() { + return new Builder(); + } + + /** Filtering predicate in which all test cases will be included and run. */ + static BiPredicate includeAll() { + return (m, c) -> true; + } + + /** + * Filtering predicate in which only those test cases which match up to the specified {@code + * mappingId} will be included and run. + */ + static BiPredicate individualMapping(int mappingId) { + return (m, c) -> c.getMappingId() == mappingId; + } + + static final class Builder { + private String retryTestsJsonResourcePath; + private RpcMethodMappings mappings; + private String host; + private BiPredicate testAllowFilter; + private final Random rand; + + public Builder() { + this.rand = resolveRand(); + } + + /** + * Set the resource path of where to resolve the retry_tests.json from + * google-cloud-conformance-tests + */ + public Builder setRetryTestsJsonResourcePath(String retryTestsJsonResourcePath) { + this.retryTestsJsonResourcePath = retryTestsJsonResourcePath; + return this; + } + + /** Set the defined mappings which are to be used in test generation */ + public Builder setMappings(RpcMethodMappings mappings) { + this.mappings = requireNonNull(mappings, "mappings must be non null"); + return this; + } + + /** Set the host string of where the testbench will be available during a test run */ + public Builder setHost(String host) { + this.host = host; + return this; + } + + /** + * Set the allow filter for determining if a particular {@link RpcMethod} and {@link + * TestRetryConformance} should be included in the generated test suite. + */ + public Builder setTestAllowFilter( + BiPredicate testAllowFilter) { + this.testAllowFilter = requireNonNull(testAllowFilter, "testAllowFilter must be non null"); + return this; + } + + public RetryTestCaseResolver build() { + return new RetryTestCaseResolver( + requireNonNull( + retryTestsJsonResourcePath, "retryTestsJsonResourcePath must be non null"), + requireNonNull(mappings, "mappings must be non null"), + requireNonNull(testAllowFilter, "testAllowList must be non null"), + rand, + host); + } + + /** + * As part of test generation and execution we are shuffling the order to ensure there is no + * ordering dependency between individual cases. Given this fact, we report the seed used for + * performing the shuffle. If an explicit seed is provided via environment variable that will + * take precedence. + */ + private static Random resolveRand() { + try { + long seed; + if (HEX_SHUFFLE_SEED_OVERRIDE != null) { + LOGGER.info( + "Shuffling test order using Random with override seed: " + + HEX_SHUFFLE_SEED_OVERRIDE); + seed = new BigInteger(HEX_SHUFFLE_SEED_OVERRIDE.replace("0x", ""), 16).longValue(); + } else { + seed = + SecureRandom.getInstanceStrong() + .longs(100) + .reduce((first, second) -> second) + .orElseThrow( + () -> { + throw new IllegalStateException("Unable to generate seed"); + }); + String msg = + String.format("Shuffling test order using Random with seed: 0x%016X", seed); + LOGGER.info(msg); + } + return new Random(seed); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + } + } + + /** + * Simple typed tuple class to bind together a {@link TestRetryConformance} and {@link + * RpcMethodMapping} during resolution. + */ + private static final class RetryTestCase { + private final TestRetryConformance testRetryConformance; + private final RpcMethodMapping rpcMethodMapping; + + RetryTestCase(TestRetryConformance testRetryConformance, RpcMethodMapping rpcMethodMapping) { + this.testRetryConformance = testRetryConformance; + this.rpcMethodMapping = rpcMethodMapping; + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ParallelParameterized.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ParallelParameterized.java new file mode 100644 index 0000000000..3d27ab902d --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ParallelParameterized.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Phaser; +import java.util.concurrent.ThreadFactory; +import java.util.logging.Logger; +import org.junit.runners.Parameterized; +import org.junit.runners.model.RunnerScheduler; + +/** + * Extends off the provided {@link Parameterized} runner provided by junit, only augmenting which + * scheduler is used so that tests can run in parallel by using a thread pool. + */ +public final class ParallelParameterized extends Parameterized { + + public ParallelParameterized(Class klass) throws Throwable { + super(klass); + this.setScheduler(new ParallelScheduler()); + } + + private static class ParallelScheduler implements RunnerScheduler { + private static final Logger LOGGER = Logger.getLogger(ParallelScheduler.class.getName()); + + private final Phaser childCounter; + private final ExecutorService executorService; + + private ParallelScheduler() { + ThreadFactory threadFactory = + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("parallel-test-runner-%02d") + .build(); + // attempt to leave some space for the testbench server running alongside these tests + int coreCount = Runtime.getRuntime().availableProcessors() - 2; + int threadCount = Math.max(2, coreCount); + LOGGER.info("Using up to " + threadCount + " threads to run tests."); + executorService = Executors.newFixedThreadPool(threadCount, threadFactory); + childCounter = new Phaser(); + } + + @Override + public void schedule(Runnable childStatement) { + childCounter.register(); + executorService.submit( + () -> { + try { + childStatement.run(); + } finally { + childCounter.arrive(); + } + }); + } + + @Override + public void finished() { + try { + childCounter.awaitAdvance(0); + } finally { + executorService.shutdownNow(); + } + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RetryTestFixture.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RetryTestFixture.java new file mode 100644 index 0000000000..cb0415c1e7 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RetryTestFixture.java @@ -0,0 +1,163 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static org.junit.Assert.assertTrue; + +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.FixedHeaderProvider; +import com.google.cloud.NoCredentials; +import com.google.cloud.conformance.storage.v1.InstructionList; +import com.google.cloud.conformance.storage.v1.Method; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import com.google.cloud.storage.conformance.retry.TestBench.RetryTestResource; +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import java.util.Map; +import java.util.logging.Logger; +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +/** + * A JUnit 4 {@link TestRule} which integrates with {@link TestBench} and {@link + * TestRetryConformance} to provide transparent lifecycle integration of setup/validation/cleanup of + * {@code /retry_test} resources. This rule expects to be bound as an {@link org.junit.Rule @Rule} + * field. + * + *

Provides pre-configured instances of {@link Storage} for setup/teardown & test. + */ +final class RetryTestFixture implements TestRule { + private static final Logger LOGGER = Logger.getLogger(RetryTestFixture.class.getName()); + + private final CleanupStrategy cleanupStrategy; + private final TestBench testBench; + private final TestRetryConformance testRetryConformance; + + private RetryTestResource retryTest; + private Storage nonTestStorage; + private Storage testStorage; + + RetryTestFixture( + CleanupStrategy cleanupStrategy, + TestBench testBench, + TestRetryConformance testRetryConformance) { + this.cleanupStrategy = cleanupStrategy; + this.testBench = testBench; + this.testRetryConformance = testRetryConformance; + } + + public Storage getNonTestStorage() { + if (nonTestStorage == null) { + this.nonTestStorage = newStorage(false); + } + return nonTestStorage; + } + + public Storage getTestStorage() { + if (testStorage == null) { + this.testStorage = newStorage(true); + } + return testStorage; + } + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + boolean testSuccess = false; + boolean testSkipped = false; + try { + LOGGER.finer("Setting up retry_test resource..."); + RetryTestResource retryTestResource = + newRetryTestResource( + testRetryConformance.getMethod(), testRetryConformance.getInstruction()); + retryTest = testBench.createRetryTest(retryTestResource); + LOGGER.fine("Setting up retry_test resource complete"); + base.evaluate(); + testSuccess = true; + } catch (AssumptionViolatedException e) { + testSkipped = true; + throw e; + } finally { + LOGGER.fine("Verifying end state of retry_test resource..."); + try { + if (testSuccess && retryTest != null) { + RetryTestResource postTestState = testBench.getRetryTest(retryTest); + assertTrue("expected completed to be true, but was false", postTestState.completed); + } + } finally { + LOGGER.fine("Verifying end state of retry_test resource complete"); + if ((shouldCleanup(testSuccess, testSkipped)) && retryTest != null) { + testBench.deleteRetryTest(retryTest); + retryTest = null; + } + } + } + } + }; + } + + private boolean shouldCleanup(boolean testSuccess, boolean testSkipped) { + return cleanupStrategy == CleanupStrategy.ALWAYS + || ((testSuccess || testSkipped) && cleanupStrategy == CleanupStrategy.ONLY_ON_SUCCESS); + } + + private static RetryTestResource newRetryTestResource(Method m, InstructionList l) { + RetryTestResource resource = new RetryTestResource(); + resource.instructions = new JsonObject(); + JsonArray instructions = new JsonArray(); + for (String s : l.getInstructionsList()) { + instructions.add(s); + } + resource.instructions.add(m.getName(), instructions); + return resource; + } + + private Storage newStorage(boolean forTest) { + StorageOptions.Builder builder = + StorageOptions.newBuilder() + .setHost(testBench.getBaseUri()) + .setCredentials(NoCredentials.getInstance()) + .setProjectId("conformance-tests"); + if (forTest) { + builder.setHeaderProvider( + new FixedHeaderProvider() { + @Override + public Map getHeaders() { + return ImmutableMap.of( + "x-retry-test-id", retryTest.id, "User-Agent", "java-conformance-tests/"); + } + }); + } else { + builder + .setHeaderProvider( + new FixedHeaderProvider() { + @Override + public Map getHeaders() { + return ImmutableMap.of("User-Agent", "java-conformance-tests/"); + } + }) + .setRetrySettings(RetrySettings.newBuilder().setMaxAttempts(1).build()); + } + return builder.build().getService(); + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethod.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethod.java new file mode 100644 index 0000000000..2227e6bf76 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethod.java @@ -0,0 +1,166 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +interface RpcMethod { + + String getFullyQualifiedMethodName(); + + /** + * Enumerate the hierarchy of storage rpc methods. + * + *

These class names intentionally do not follow java convention, because they are mapping + * directly to lower level values. + */ + final class storage { + private static String getFullQualifiedMethodName(Enum e) { + return String.format("storage.%s.%s", e.getClass().getSimpleName(), e.name()); + } + + enum bucket_acl implements RpcMethod { + delete, + get, + insert, + list, + patch, + update; + + @Override + public String getFullyQualifiedMethodName() { + return getFullQualifiedMethodName(this); + } + } + + enum buckets implements RpcMethod { + delete, + get, + insert, + list, + patch, + update, + getIamPolicy, + lockRetentionPolicy, + setIamPolicy, + testIamPermissions; + + @Override + public String getFullyQualifiedMethodName() { + return getFullQualifiedMethodName(this); + } + } + + enum default_object_acl implements RpcMethod { + delete, + get, + insert, + list, + patch, + update; + + @Override + public String getFullyQualifiedMethodName() { + return getFullQualifiedMethodName(this); + } + } + + enum hmacKey implements RpcMethod { + delete, + get, + list, + update, + create; + + @Override + public String getFullyQualifiedMethodName() { + return getFullQualifiedMethodName(this); + } + } + + enum notifications implements RpcMethod { + delete, + get, + insert, + list; + + @Override + public String getFullyQualifiedMethodName() { + return getFullQualifiedMethodName(this); + } + } + + enum object_acl implements RpcMethod { + delete, + get, + insert, + list, + patch, + update; + + @Override + public String getFullyQualifiedMethodName() { + return getFullQualifiedMethodName(this); + } + } + + enum objects implements RpcMethod { + delete, + get, + insert, + list, + patch, + update, + compose, + rewrite, + copy; + + @Override + public String getFullyQualifiedMethodName() { + return getFullQualifiedMethodName(this); + } + } + + enum serviceaccount implements RpcMethod { + get; + + @Override + public String getFullyQualifiedMethodName() { + return getFullQualifiedMethodName(this); + } + } + + // create a map, which can be used to do a reverse lookup of an RpcMethod by its associated + // string value. + static final Map lookup = + Stream.>of( + Arrays.stream(bucket_acl.values()), + Arrays.stream(buckets.values()), + Arrays.stream(default_object_acl.values()), + Arrays.stream(hmacKey.values()), + Arrays.stream(notifications.values()), + Arrays.stream(object_acl.values()), + Arrays.stream(objects.values()), + Arrays.stream(serviceaccount.values())) + .flatMap(Function.identity()) // .flatten() + .collect(Collectors.toMap(RpcMethod::getFullyQualifiedMethodName, Function.identity())); + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMapping.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMapping.java new file mode 100644 index 0000000000..28e5c6edf5 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMapping.java @@ -0,0 +1,199 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Objects.requireNonNull; +import static org.junit.Assert.fail; + +import com.google.cloud.storage.StorageException; +import com.google.cloud.storage.conformance.retry.CtxFunctions.ResourceSetup; +import com.google.cloud.storage.conformance.retry.Functions.CtxFunction; +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.Immutable; +import java.util.HashSet; +import java.util.function.Predicate; +import org.junit.AssumptionViolatedException; + +/** + * Immutable class which represents a mapping between an {@link RpcMethod} and a method in the + * public {@code com.google.cloud.storage} API. + * + *

This class defines a semi-declarative why in which mappings can be declared independent of the + * actual environment & state necessary to actually invoke a method. + * + * @see ITRetryConformanceTest#test() + * @see RpcMethodMappings + */ +@Immutable +final class RpcMethodMapping { + + private final int mappingId; + private final RpcMethod method; + private final Predicate applicable; + private final CtxFunction setup; + private final CtxFunction test; + private final CtxFunction tearDown; + + RpcMethodMapping( + int mappingId, + RpcMethod method, + Predicate applicable, + CtxFunction setup, + CtxFunction test, + CtxFunction tearDown) { + this.mappingId = mappingId; + this.method = method; + this.applicable = applicable; + this.setup = setup; + this.test = test; + this.tearDown = tearDown; + } + + public int getMappingId() { + return mappingId; + } + + public RpcMethod getMethod() { + return method; + } + + public Predicate getApplicable() { + return applicable; + } + + public CtxFunction getSetup() { + return setup; + } + + public CtxFunction getTest() { + return (ctx, c) -> { + if (c.isExpectSuccess()) { + return test.apply(ctx, c); + } else { + try { + test.apply(ctx, c); + fail("expected failure, but succeeded"); + } catch (StorageException e) { + // We expect an exception to be thrown by mapping and test retry conformance config + // Verify that the exception we received is actually what we expect. + boolean matchExpectedCode = false; + int code = e.getCode(); + HashSet instructions = newHashSet(c.getInstruction().getInstructionsList()); + if (instructions.contains("return-503") && code == 503) { + matchExpectedCode = true; + } + if (instructions.contains("return-400") && code == 400) { + matchExpectedCode = true; + } + if (instructions.contains("return-401") && code == 401) { + matchExpectedCode = true; + } + if (instructions.contains("return-reset-connection") && code == 0) { + matchExpectedCode = true; + } + + if (matchExpectedCode) { + return ctx; + } else { + throw e; + } + } + } + throw new IllegalStateException( + "Unable to determine applicability of mapping for provided TestCaseConfig"); + }; + } + + public CtxFunction getTearDown() { + return tearDown; + } + + public Builder toBuilder() { + return new Builder(mappingId, method, applicable, setup, test, tearDown); + } + + static Builder newBuilder(int mappingId, RpcMethod method) { + Preconditions.checkArgument(mappingId >= 1, "mappingId must be >= 1, but was %d", mappingId); + return new Builder(mappingId, method); + } + + static RpcMethodMapping notImplemented(RpcMethod method) { + return new Builder(0, method) + .withTest( + (s, c) -> { + throw new AssumptionViolatedException("not implemented"); + }) + .build(); + } + + static final class Builder { + + private final int mappingId; + private final RpcMethod method; + private final Predicate applicable; + private final CtxFunction setup; + private final CtxFunction test; + private CtxFunction tearDown; + + Builder(int mappingId, RpcMethod method) { + this(mappingId, method, x -> true, ResourceSetup.defaultSetup, null, CtxFunction.identity()); + } + + private Builder( + int mappingId, + RpcMethod method, + Predicate applicable, + CtxFunction setup, + CtxFunction test, + CtxFunction tearDown) { + this.mappingId = mappingId; + this.method = method; + this.applicable = applicable; + this.setup = setup; + this.test = test; + this.tearDown = tearDown; + } + + public Builder withApplicable(Predicate applicable) { + return new Builder(mappingId, method, applicable, setup, test, tearDown); + } + + public Builder withSetup(CtxFunction setup) { + return new Builder(mappingId, method, applicable, setup, null, tearDown); + } + + public Builder withTest(CtxFunction test) { + return new Builder(mappingId, method, applicable, setup, test, CtxFunction.identity()); + } + + public Builder withTearDown(CtxFunction tearDown) { + this.tearDown = tearDown; + return this; + } + + public RpcMethodMapping build() { + return new RpcMethodMapping( + mappingId, + requireNonNull(method, "method must be non null"), + requireNonNull(applicable, "applicable must be non null"), + requireNonNull(setup, "setup must be non null"), + requireNonNull(test, "test must be non null"), + requireNonNull(tearDown, "tearDown must be non null")); + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMappings.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMappings.java new file mode 100644 index 0000000000..3644fbb0ad --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/RpcMethodMappings.java @@ -0,0 +1,1942 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static com.google.cloud.storage.conformance.retry.CtxFunctions.Local.blobIdWithoutGeneration; +import static com.google.cloud.storage.conformance.retry.CtxFunctions.Local.blobInfoWithGenerationZero; +import static com.google.cloud.storage.conformance.retry.CtxFunctions.Local.blobInfoWithoutGeneration; +import static com.google.cloud.storage.conformance.retry.CtxFunctions.Local.bucketInfo; +import static com.google.common.base.Predicates.not; +import static com.google.common.collect.Lists.newArrayList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.Policy; +import com.google.cloud.ReadChannel; +import com.google.cloud.WriteChannel; +import com.google.cloud.storage.Acl.User; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.HmacKey.HmacKeyState; +import com.google.cloud.storage.HttpMethod; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.Storage.BlobGetOption; +import com.google.cloud.storage.Storage.BlobSourceOption; +import com.google.cloud.storage.Storage.BlobTargetOption; +import com.google.cloud.storage.Storage.BlobWriteOption; +import com.google.cloud.storage.Storage.BucketSourceOption; +import com.google.cloud.storage.Storage.BucketTargetOption; +import com.google.cloud.storage.Storage.ComposeRequest; +import com.google.cloud.storage.Storage.CopyRequest; +import com.google.cloud.storage.Storage.SignUrlOption; +import com.google.cloud.storage.Storage.UriScheme; +import com.google.cloud.storage.conformance.retry.CtxFunctions.Local; +import com.google.cloud.storage.conformance.retry.CtxFunctions.Rpc; +import com.google.cloud.storage.conformance.retry.RpcMethod.storage.bucket_acl; +import com.google.cloud.storage.conformance.retry.RpcMethod.storage.buckets; +import com.google.cloud.storage.conformance.retry.RpcMethod.storage.default_object_acl; +import com.google.cloud.storage.conformance.retry.RpcMethod.storage.hmacKey; +import com.google.cloud.storage.conformance.retry.RpcMethod.storage.object_acl; +import com.google.cloud.storage.conformance.retry.RpcMethod.storage.objects; +import com.google.cloud.storage.conformance.retry.RpcMethod.storage.serviceaccount; +import com.google.cloud.storage.conformance.retry.RpcMethodMappings.Mappings.BucketAcl; +import com.google.cloud.storage.conformance.retry.RpcMethodMappings.Mappings.Buckets; +import com.google.cloud.storage.conformance.retry.RpcMethodMappings.Mappings.DefaultObjectAcl; +import com.google.cloud.storage.conformance.retry.RpcMethodMappings.Mappings.HmacKey; +import com.google.cloud.storage.conformance.retry.RpcMethodMappings.Mappings.Notification; +import com.google.cloud.storage.conformance.retry.RpcMethodMappings.Mappings.ObjectAcl; +import com.google.cloud.storage.conformance.retry.RpcMethodMappings.Mappings.Objects; +import com.google.cloud.storage.conformance.retry.RpcMethodMappings.Mappings.ServiceAccount; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Sets; +import com.google.common.io.ByteStreams; +import com.google.errorprone.annotations.Immutable; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map.Entry; +import java.util.OptionalInt; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * A class which serves to try and organize all of the {@link RpcMethodMapping} for the retry + * conformance tests. + * + *

Individual mappings are grouped via inner classes corresponding to the {@link RpcMethod} for + * which they are defined. + * + *

As part of construction mappingIds are enforced to be unique, throwing an error if not. + */ +@Immutable +@SuppressWarnings("Guava") +final class RpcMethodMappings { + private static final Logger LOGGER = Logger.getLogger(RpcMethodMappings.class.getName()); + + static final int _2MiB = 2 * 1024 * 1024; + final Multimap funcMap; + + RpcMethodMappings() { + ArrayList a = new ArrayList<>(); + + BucketAcl.delete(a); + BucketAcl.get(a); + BucketAcl.insert(a); + BucketAcl.list(a); + BucketAcl.patch(a); + + Buckets.delete(a); + Buckets.get(a); + Buckets.insert(a); + Buckets.list(a); + Buckets.patch(a); + Buckets.update(a); + Buckets.getIamPolicy(a); + Buckets.lockRetentionPolicy(a); + Buckets.setIamPolicy(a); + Buckets.testIamPermission(a); + + DefaultObjectAcl.delete(a); + DefaultObjectAcl.get(a); + DefaultObjectAcl.insert(a); + DefaultObjectAcl.list(a); + DefaultObjectAcl.patch(a); + DefaultObjectAcl.update(a); + + HmacKey.delete(a); + HmacKey.get(a); + HmacKey.list(a); + HmacKey.update(a); + HmacKey.create(a); + + Notification.delete(a); + Notification.get(a); + Notification.insert(a); + Notification.list(a); + + ObjectAcl.delete(a); + ObjectAcl.get(a); + ObjectAcl.insert(a); + ObjectAcl.list(a); + ObjectAcl.patch(a); + ObjectAcl.update(a); + + Objects.delete(a); + Objects.get(a); + Objects.insert(a); + Objects.list(a); + Objects.patch(a); + Objects.update(a); + Objects.compose(a); + Objects.rewrite(a); + Objects.copy(a); + + ServiceAccount.get(a); + ServiceAccount.put(a); + + validateMappingDefinitions(a); + + funcMap = Multimaps.index(a, RpcMethodMapping::getMethod); + reportMappingSummary(); + } + + public Collection get(RpcMethod key) { + return funcMap.get(key); + } + + public Set differenceMappingIds(Set usedMappingIds) { + return Sets.difference( + funcMap.values().stream().map(RpcMethodMapping::getMappingId).collect(Collectors.toSet()), + usedMappingIds); + } + + private void validateMappingDefinitions(ArrayList a) { + ListMultimap idMappings = + MultimapBuilder.hashKeys() + .arrayListValues() + .build(Multimaps.index(a, RpcMethodMapping::getMappingId)); + String duplicateIds = + idMappings.asMap().entrySet().stream() + .filter(e -> e.getValue().size() > 1) + .map(Entry::getKey) + .map(i -> Integer.toString(i)) + .collect(Collectors.joining(", ")); + if (!duplicateIds.isEmpty()) { + String message = "duplicate mapping ids present: [" + duplicateIds + "]"; + throw new IllegalStateException(message); + } + } + + private void reportMappingSummary() { + int mappingCount = funcMap.values().stream().mapToInt(m -> 1).sum(); + LOGGER.info("Current total number of mappings defined: " + mappingCount); + String counts = + funcMap.asMap().entrySet().stream() + .map( + e -> { + RpcMethod rpcMethod = e.getKey(); + Collection mappings = e.getValue(); + return String.format( + "\t%s.%s: %d", + rpcMethod + .getClass() + .getName() + .replace("com.google.cloud.storage.conformance.retry.RpcMethod$", "") + .replace("$", "."), + rpcMethod, + mappings.size()); + }) + .sorted() + .collect(Collectors.joining("\n", "\n", "")); + LOGGER.info("Current number of mappings per rpc method: " + counts); + OptionalInt max = + funcMap.values().stream().map(RpcMethodMapping::getMappingId).mapToInt(i -> i).max(); + if (max.isPresent()) { + LOGGER.info(String.format("Current max mapping index is: %d%n", max.getAsInt())); + } else { + throw new IllegalStateException("No mappings defined"); + } + } + + static final class Mappings { + + static final class BucketAcl { + + private static void delete(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(1, bucket_acl.delete) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .deleteAcl(c.getBucketName(), User.ofAllUsers())))) + .build()); // TODO: Why does this exist, varargs should suffice + a.add( + RpcMethodMapping.newBuilder(2, bucket_acl.delete) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .deleteAcl( + c.getBucketName(), + User.ofAllUsers(), + BucketSourceOption.userProject(c.getUserProject()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(87, bucket_acl.delete) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .deleteAcl(state.getAcl().getEntity()))))) + .build()); + } + + private static void get(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(3, bucket_acl.get) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage().getAcl(c.getBucketName(), User.ofAllUsers())))) + .build()); // TODO: Why does this exist, varargs should suffice + a.add( + RpcMethodMapping.newBuilder(4, bucket_acl.get) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .getAcl( + c.getBucketName(), + User.ofAllUsers(), + BucketSourceOption.userProject(c.getUserProject()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(88, bucket_acl.get) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state.getBucket().getAcl(state.getAcl().getEntity()))))) + .build()); + } + + private static void insert(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(5, bucket_acl.insert) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage().createAcl(c.getBucketName(), state.getAcl())))) + .build()); // TODO: Why does this exist, varargs should suffice + a.add( + RpcMethodMapping.newBuilder(6, bucket_acl.insert) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createAcl( + c.getBucketName(), + state.getAcl(), + BucketSourceOption.userProject(c.getUserProject()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(89, bucket_acl.insert) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with(state.getBucket().createAcl(state.getAcl()))))) + .build()); + } + + private static void list(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(7, bucket_acl.list) + .withTest( + (ctx, c) -> + ctx.map( + state -> state.withAcls(ctx.getStorage().listAcls(c.getBucketName())))) + .build()); // TODO: Why does this exist, varargs should suffice + a.add( + RpcMethodMapping.newBuilder(8, bucket_acl.list) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.withAcls( + ctx.getStorage() + .listAcls( + c.getBucketName(), + BucketSourceOption.userProject(c.getUserProject()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(90, bucket_acl.list) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map(state -> state.withAcls(state.getBucket().listAcls())))) + .build()); + } + + private static void patch(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(9, bucket_acl.patch) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage().updateAcl(c.getBucketName(), state.getAcl())))) + .build()); // TODO: Why does this exist, varargs should suffice + a.add( + RpcMethodMapping.newBuilder(10, bucket_acl.patch) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .updateAcl( + c.getBucketName(), + state.getAcl(), + BucketSourceOption.userProject(c.getUserProject()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(91, bucket_acl.patch) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with(state.getBucket().updateAcl(state.getAcl()))))) + .build()); + } + } + + static final class Buckets { + private static void delete(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(11, buckets.delete) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .delete( + c.getBucketName(), + BucketSourceOption.userProject(c.getUserProject()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(92, buckets.delete) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> ctx.map(state -> state.with(state.getBucket().delete())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(93, buckets.delete) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .delete(Bucket.BucketSourceOption.metagenerationMatch())))) + .build()); + } + + private static void get(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(12, buckets.get) + .withTest( + (ctx, c) -> + ctx.map(state -> state.with(ctx.getStorage().get(c.getBucketName())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(94, buckets.get) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest((ctx, c) -> ctx.map(state -> state.with(state.getBucket().exists()))) + .build()); + a.add( + RpcMethodMapping.newBuilder(95, buckets.get) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .exists(Bucket.BucketSourceOption.metagenerationMatch())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(96, buckets.get) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest((ctx, c) -> ctx.map(state -> state.with(state.getBucket().reload()))) + .build()); + a.add( + RpcMethodMapping.newBuilder(97, buckets.get) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .reload(Bucket.BucketSourceOption.metagenerationMatch())))) + .build()); + } + + private static void insert(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(14, buckets.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + bucketInfo.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with(ctx.getStorage().create(state.getBucketInfo()))))) + .build()); + } + + private static void list(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(15, buckets.list) + .withTest( + (ctx, c) -> + ctx.map(state -> state.consume(ctx.getStorage().list(c.getBucketName())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(98, buckets.list) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest((ctx, c) -> ctx.map(state -> state.consume(state.getBucket().list()))) + .build()); + } + + private static void patch(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(17, buckets.patch) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + bucketInfo.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with(ctx.getStorage().update(state.getBucketInfo()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(122, buckets.patch) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + bucketInfo.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .update( + state.getBucketInfo(), + BucketTargetOption.metagenerationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(101, buckets.patch) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .update( + BucketTargetOption.metagenerationMatch()))))) + .build()); + } + + private static void update(ArrayList a) {} + + private static void getIamPolicy(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(13, buckets.getIamPolicy) + .withTest( + (ctx, c) -> + ctx.map( + state -> state.with(ctx.getStorage().getIamPolicy(c.getBucketName())))) + .build()); + } + + private static void lockRetentionPolicy(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(16, buckets.lockRetentionPolicy) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + bucketInfo.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .lockRetentionPolicy( + state.getBucketInfo(), + BucketTargetOption.metagenerationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(99, buckets.lockRetentionPolicy) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> state.with(state.getBucket().lockRetentionPolicy())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(100, buckets.lockRetentionPolicy) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .lockRetentionPolicy( + BucketTargetOption.metagenerationMatch()))))) + .build()); + } + + private static void setIamPolicy(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(18, buckets.setIamPolicy) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .setIamPolicy( + c.getBucketName(), Policy.newBuilder().build())))) + .build()); // TODO: configure policy + } + + private static void testIamPermission(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(19, buckets.testIamPermissions) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.withTestIamPermissionsResults( + ctx.getStorage() + .testIamPermissions( + c.getBucketName(), + Collections.singletonList("todo: permissions"))))) + .build()); // TODO: configure permissions + } + } + + static final class DefaultObjectAcl { + + private static void delete(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(20, default_object_acl.delete) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .deleteDefaultAcl( + c.getBucketName(), state.getAcl().getEntity())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(102, default_object_acl.delete) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .deleteDefaultAcl(state.getAcl().getEntity()))))) + .build()); + } + + private static void get(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(21, default_object_acl.get) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .getDefaultAcl( + c.getBucketName(), state.getAcl().getEntity())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(103, default_object_acl.get) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .getDefaultAcl(state.getAcl().getEntity()))))) + .build()); + } + + private static void insert(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(22, default_object_acl.insert) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createDefaultAcl(c.getBucketName(), state.getAcl())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(104, default_object_acl.insert) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state.getBucket().createDefaultAcl(state.getAcl()))))) + .build()); + } + + private static void list(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(23, default_object_acl.list) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.withAcls( + ctx.getStorage().listDefaultAcls(c.getBucketName())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(105, default_object_acl.list) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> state.withAcls(state.getBucket().listDefaultAcls())))) + .build()); + } + + private static void patch(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(24, default_object_acl.patch) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .updateDefaultAcl(c.getBucketName(), state.getAcl())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(106, default_object_acl.patch) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state.getBucket().updateDefaultAcl(state.getAcl()))))) + .build()); + } + + private static void update(ArrayList a) {} + } + + static final class HmacKey { + + private static void delete(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(26, hmacKey.delete) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.consume( + () -> + ctx.getStorage() + .deleteHmacKey(state.getHmacKey().getMetadata())))) + .build()); + } + + private static void get(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(27, hmacKey.get) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .getHmacKey( + state.getHmacKey().getMetadata().getAccessId())))) + .build()); + } + + private static void list(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(28, hmacKey.list) + .withTest( + (ctx, c) -> ctx.map(state -> state.consume(ctx.getStorage().listHmacKeys()))) + .build()); + } + + private static void update(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(29, hmacKey.update) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .updateHmacKeyState( + state.getHmacKey().getMetadata(), + HmacKeyState.ACTIVE)))) + .build()); // TODO: what state should be used in the test? + } + + private static void create(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(25, hmacKey.create) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage().createHmacKey(state.getServiceAccount())))) + .build()); + } + } + + static final class Notification { + + private static void delete(ArrayList a) {} + + private static void get(ArrayList a) {} + + private static void insert(ArrayList a) {} + + private static void list(ArrayList a) {} + } + + static final class ObjectAcl { + + private static void delete(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(30, object_acl.delete) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .deleteAcl( + state.getBlobId(), state.getAcl().getEntity()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(62, object_acl.delete) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBlob() + .deleteAcl(state.getAcl().getEntity()))))) + .build()); + } + + private static void get(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(31, object_acl.get) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .getAcl( + state.getBlobId(), state.getAcl().getEntity()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(63, object_acl.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state.getBlob().getAcl(state.getAcl().getEntity()))))) + .build()); + } + + private static void insert(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(32, object_acl.insert) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createAcl(state.getBlobId(), state.getAcl()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(64, object_acl.insert) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with(state.getBlob().createAcl(state.getAcl()))))) + .build()); + } + + private static void list(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(33, object_acl.list) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.withAcls(ctx.getStorage().listAcls(state.getBlobId()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(65, object_acl.list) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.map(state -> state.withAcls(state.getBlob().listAcls())))) + .build()); + } + + private static void patch(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(34, object_acl.patch) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .updateAcl(state.getBlobId(), state.getAcl()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(66, object_acl.patch) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with(state.getBlob().updateAcl(state.getAcl()))))) + .build()); + } + + private static void update(ArrayList a) {} + } + + static final class Objects { + + private static void delete(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(36, objects.delete) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> state.with(ctx.getStorage().delete(state.getBlobId()))))) + .build()); // TODO: Why does this exist, varargs should suffice + a.add( + RpcMethodMapping.newBuilder(37, objects.delete) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .delete( + state.getBlobId(), + BlobSourceOption.metagenerationMatch(1L)))))) + .build()); // TODO: Correct arg? + a.add( + RpcMethodMapping.newBuilder(38, objects.delete) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .delete( + state.getBlobId().getBucket(), + state.getBlobId().getName(), + BlobSourceOption.metagenerationMatch(1L)))))) + .build()); // TODO: Correct arg? + a.add( + RpcMethodMapping.newBuilder(67, objects.delete) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen((ctx, c) -> ctx.peek(state -> state.getBlob().delete()))) + .build()); + a.add( + RpcMethodMapping.newBuilder(68, objects.delete) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> + state + .getBlob() + .delete(Blob.BlobSourceOption.metagenerationMatch())))) + .build()); + } + + private static void get(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(39, objects.get) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map(state -> state.with(ctx.getStorage().get(state.getBlobId()))))) + .build()); // TODO: Why does this exist, varargs should suffice + a.add( + RpcMethodMapping.newBuilder(239, objects.get) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + (ctx, c) -> + ctx.peek(state -> ctx.getStorage().get(state.getBlob().getBlobId()))) + .withTearDown( + CtxFunctions.ResourceTeardown.object.andThen( + CtxFunctions.ResourceTeardown.bucket)) + .build()); + a.add( + RpcMethodMapping.newBuilder(40, objects.get) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .get( + state.getBlobId(), + BlobGetOption.metagenerationMatch(1L)))))) + .build()); // TODO: Correct arg? + a.add( + RpcMethodMapping.newBuilder(41, objects.get) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .get( + state.getBlobId().getBucket(), + state.getBlobId().getName(), + BlobGetOption.metagenerationMatch(1L)))))) + .build()); // TODO: Correct arg? + a.add( + RpcMethodMapping.newBuilder(42, objects.get) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .readAllBytes( + state.getBlobId(), + BlobSourceOption.metagenerationMatch(1L)))))) + .build()); // TODO: Correct arg? + a.add( + RpcMethodMapping.newBuilder(43, objects.get) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .readAllBytes( + state.getBlobId().getBucket(), + state.getBlobId().getName(), + BlobSourceOption.metagenerationMatch(1L)))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(44, objects.get) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.peek( + state -> { + ReadChannel reader = + ctx.getStorage().reader(ctx.getState().getBlobId()); + WritableByteChannel write = + Channels.newChannel(NullOutputStream.INSTANCE); + ByteStreams.copy(reader, write); + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(45, objects.get) + .withTest( + blobIdWithoutGeneration.andThen( + (ctx, c) -> + ctx.peek( + state -> { + ReadChannel reader = + ctx.getStorage() + .reader( + ctx.getState().getBlobId().getBucket(), + ctx.getState().getBlobId().getName()); + WritableByteChannel write = + Channels.newChannel(NullOutputStream.INSTANCE); + ByteStreams.copy(reader, write); + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(60, objects.get) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> ctx.peek(state -> assertTrue(state.getBlob().exists())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(61, objects.get) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> + assertTrue( + state + .getBlob() + .exists(Blob.BlobSourceOption.generationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(69, objects.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> { + Path tmpOutFile = + Files.createTempFile(c.getMethod().getName(), ".txt"); + state + .getBlob() + .downloadTo( + tmpOutFile); // TODO: Why does this exist, varargs + // should suffice + byte[] downloadedBytes = Files.readAllBytes(tmpOutFile); + assertEquals(c.getHelloWorldUtf8Bytes(), downloadedBytes); + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(70, objects.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> { + Path tmpOutFile = + Files.createTempFile(c.getMethod().getName(), ".txt"); + state + .getBlob() + .downloadTo( + tmpOutFile, + Blob.BlobSourceOption.metagenerationMatch()); + byte[] downloadedBytes = Files.readAllBytes(tmpOutFile); + assertEquals(c.getHelloWorldUtf8Bytes(), downloadedBytes); + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(71, objects.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + state.getBlob().downloadTo(baos); + byte[] downloadedBytes = baos.toByteArray(); + assertEquals(c.getHelloWorldUtf8Bytes(), downloadedBytes); + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(72, objects.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + state + .getBlob() + .downloadTo( + baos, Blob.BlobSourceOption.metagenerationMatch()); + byte[] downloadedBytes = baos.toByteArray(); + assertEquals(c.getHelloWorldUtf8Bytes(), downloadedBytes); + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(73, objects.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> { + byte[] downloadedBytes = state.getBlob().getContent(); + assertEquals(c.getHelloWorldUtf8Bytes(), downloadedBytes); + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(74, objects.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> { + byte[] downloadedBytes = + state + .getBlob() + .getContent( + Blob.BlobSourceOption.metagenerationMatch()); + assertEquals(c.getHelloWorldUtf8Bytes(), downloadedBytes); + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(75, objects.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen((ctx, c) -> ctx.peek(state -> state.getBlob().reload()))) + .build()); + a.add( + RpcMethodMapping.newBuilder(76, objects.get) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> + state + .getBlob() + .reload(Blob.BlobSourceOption.metagenerationMatch())))) + .build()); + a.add( + RpcMethodMapping.newBuilder(107, objects.get) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> state.with(state.getBucket().get(c.getObjectName()))))) + .build()); // TODO: Fill out permutations here + } + + private static void insert(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(46, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithGenerationZero.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .create( + ctx.getState().getBlobInfo(), + c.getHelloWorldUtf8Bytes(), + BlobTargetOption.generationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(47, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithGenerationZero.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .create( + ctx.getState().getBlobInfo(), + c.getHelloWorldUtf8Bytes(), + 0, + c.getHelloWorldUtf8Bytes().length / 2, + BlobTargetOption.generationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(48, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithGenerationZero.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .create( + ctx.getState().getBlobInfo(), + new ByteArrayInputStream( + c.getHelloWorldUtf8Bytes()), + BlobWriteOption.generationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(49, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithGenerationZero.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createFrom( + ctx.getState().getBlobInfo(), + new ByteArrayInputStream( + c.getHelloWorldUtf8Bytes()), + BlobWriteOption.generationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(50, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithGenerationZero.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createFrom( + ctx.getState().getBlobInfo(), + c.getHelloWorldFilePath(), + BlobWriteOption.generationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(51, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithGenerationZero.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createFrom( + ctx.getState().getBlobInfo(), + c.getHelloWorldFilePath(), + _2MiB, + BlobWriteOption.generationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(52, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithoutGeneration.andThen( + (ctx, c) -> + ctx.peek( + state -> { + try (WriteChannel writer = + ctx.getStorage().writer(ctx.getState().getBlobInfo())) { + writer.write(ByteBuffer.wrap(c.getHelloWorldUtf8Bytes())); + } + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(53, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithGenerationZero.andThen( + (ctx, c) -> + ctx.peek( + state -> { + try (WriteChannel writer = + ctx.getStorage() + .writer( + ctx.getState().getBlobInfo(), + BlobWriteOption.generationMatch())) { + writer.write(ByteBuffer.wrap(c.getHelloWorldUtf8Bytes())); + } + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(54, objects.insert) + .withTest( + blobInfoWithoutGeneration.andThen( + (ctx, c) -> + ctx.peek( + state -> { + Storage storage = ctx.getStorage(); + URL signedUrl = + storage.signUrl( + state.getBlobInfo(), + 1, + TimeUnit.HOURS, + SignUrlOption.httpMethod(HttpMethod.POST), + SignUrlOption.withBucketBoundHostname( + c.getHost(), UriScheme.HTTP), + SignUrlOption.withExtHeaders( + ImmutableMap.of("x-goog-resumable", "start")), + SignUrlOption.signWith(c.getServiceAccountSigner()), + SignUrlOption.withV2Signature()); + try (WriteChannel writer = storage.writer(signedUrl)) { + writer.write(ByteBuffer.wrap(c.getHelloWorldUtf8Bytes())); + } + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(77, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithoutGeneration + .andThen(Rpc.createEmptyBlob) + .andThen( + (ctx, c) -> + ctx.peek( + state -> { + try (WriteChannel writer = state.getBlob().writer()) { + writer.write(ByteBuffer.wrap(c.getHelloWorldUtf8Bytes())); + } + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(78, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + blobInfoWithoutGeneration + .andThen(Rpc.createEmptyBlob) + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.peek( + state -> { + try (WriteChannel writer = + state + .getBlob() + .writer(BlobWriteOption.generationMatch())) { + writer.write(ByteBuffer.wrap(c.getHelloWorldUtf8Bytes())); + } + }))) + .build()); + a.add( + RpcMethodMapping.newBuilder(108, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .create(c.getObjectName(), c.getHelloWorldUtf8Bytes())))) + .build()); // TODO: Fill out permutations here + a.add( + RpcMethodMapping.newBuilder(109, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .create( + c.getObjectName(), + c.getHelloWorldUtf8Bytes(), + "text/plain);charset=utf-8")))) + .build()); + a.add( + RpcMethodMapping.newBuilder(110, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .create( + c.getObjectName(), + new ByteArrayInputStream(c.getHelloWorldUtf8Bytes()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(111, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .create( + c.getObjectName(), + new ByteArrayInputStream(c.getHelloWorldUtf8Bytes()), + "text/plain);charset=utf-8")))) + .build()); + a.add( + RpcMethodMapping.newBuilder(112, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + blobInfoWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .create( + ctx.getState().getBlobInfo(), + c.getHelloWorldUtf8Bytes()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(113, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + blobInfoWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .create( + ctx.getState().getBlobInfo(), + c.getHelloWorldUtf8Bytes(), + 0, + c.getHelloWorldUtf8Bytes().length / 2))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(114, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + blobInfoWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .create( + ctx.getState().getBlobInfo(), + new ByteArrayInputStream( + c.getHelloWorldUtf8Bytes())))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(115, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + blobInfoWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createFrom( + ctx.getState().getBlobInfo(), + new ByteArrayInputStream( + c.getHelloWorldUtf8Bytes())))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(116, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + blobInfoWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createFrom( + ctx.getState().getBlobInfo(), + c.getHelloWorldFilePath()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(117, objects.insert) + .withApplicable(not(TestRetryConformance::isPreconditionsProvided)) + .withTest( + blobInfoWithoutGeneration.andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .createFrom( + ctx.getState().getBlobInfo(), + c.getHelloWorldFilePath(), + _2MiB))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(118, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .create( + c.getObjectName(), + c.getHelloWorldUtf8Bytes(), + Bucket.BlobTargetOption.generationMatch(1L)))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(119, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .create( + c.getObjectName(), + c.getHelloWorldUtf8Bytes(), + "text/plain);charset=utf-8", + Bucket.BlobTargetOption.generationMatch(1L)))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(120, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .create( + c.getObjectName(), + new ByteArrayInputStream( + c.getHelloWorldUtf8Bytes()), + Bucket.BlobWriteOption.generationMatch(1L)))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(121, objects.insert) + .withApplicable(TestRetryConformance::isPreconditionsProvided) + .withTest( + bucketInfo + .andThen(Rpc.bucket) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBucket() + .create( + c.getObjectName(), + new ByteArrayInputStream( + c.getHelloWorldUtf8Bytes()), + "text/plain);charset=utf-8", + Bucket.BlobWriteOption.generationMatch(1L)))))) + .build()); + } + + private static void list(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(55, objects.list) + .withTest( + (ctx, c) -> + ctx.map(state -> state.consume(ctx.getStorage().list(c.getBucketName())))) + .build()); + } + + private static void patch(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(56, objects.patch) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage().update(ctx.getState().getBlob()))))) + .build()); // TODO: Why does this exist, varargs should suffice + a.add( + RpcMethodMapping.newBuilder(57, objects.patch) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .update( + ctx.getState().getBlob(), + BlobTargetOption.metagenerationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(79, objects.patch) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen((ctx, c) -> ctx.peek(state -> state.getBlob().update()))) + .build()); + a.add( + RpcMethodMapping.newBuilder(80, objects.patch) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBlob() + .update(BlobTargetOption.generationMatch()))))) + .build()); // TODO: Correct arg? + } + + private static void update(ArrayList a) {} + + private static void compose(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(35, objects.compose) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .compose( + ComposeRequest.of( + c.getBucketName(), + newArrayList("blob-part-1", "blob-part-2"), + "blob-full"))))) + .build()); + } + + private static void rewrite(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(58, objects.rewrite) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with( + ctx.getStorage() + .copy( + CopyRequest.of( + c.getBucketName(), "blob-source", "blob-target"))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(81, objects.rewrite) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen(Local.blobCopy) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with(state.getBlob().copyTo(state.getCopyDest()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(82, objects.rewrite) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen(Local.blobCopy) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBlob() + .copyTo( + state.getCopyDest(), + Blob.BlobSourceOption.metagenerationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(83, objects.rewrite) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen(Local.blobCopy) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBlob() + .copyTo(state.getCopyDest().getBucket()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(84, objects.rewrite) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen(Local.blobCopy) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBlob() + .copyTo( + state.getCopyDest().getBucket(), + Blob.BlobSourceOption.metagenerationMatch()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(85, objects.rewrite) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen(Local.blobCopy) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBlob() + .copyTo( + state.getCopyDest().getBucket(), + state.getCopyDest().getName()))))) + .build()); + a.add( + RpcMethodMapping.newBuilder(86, objects.rewrite) + .withTest( + blobIdWithoutGeneration + .andThen(Rpc.blobWithGeneration) + .andThen(Local.blobCopy) + .andThen( + (ctx, c) -> + ctx.map( + state -> + state.with( + state + .getBlob() + .copyTo( + state.getCopyDest().getBucket(), + state.getCopyDest().getName(), + Blob.BlobSourceOption.metagenerationMatch()))))) + .build()); + } + + private static void copy(ArrayList a) {} + } + + static final class ServiceAccount { + + private static void get(ArrayList a) { + a.add( + RpcMethodMapping.newBuilder(59, serviceaccount.get) + .withTest( + (ctx, c) -> + ctx.map( + state -> + state.with(ctx.getStorage().getServiceAccount(c.getUserProject())))) + .build()); + } + + private static void put(ArrayList a) {} + } + } + + static final class NullOutputStream extends OutputStream { + private static final NullOutputStream INSTANCE = new NullOutputStream(); + + @Override + public void write(int b) {} + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/State.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/State.java new file mode 100644 index 0000000000..e1ddb808de --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/State.java @@ -0,0 +1,342 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static java.util.Objects.requireNonNull; + +import com.google.api.gax.paging.Page; +import com.google.cloud.Policy; +import com.google.cloud.storage.Acl; +import com.google.cloud.storage.Blob; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Bucket; +import com.google.cloud.storage.BucketInfo; +import com.google.cloud.storage.CopyWriter; +import com.google.cloud.storage.HmacKey; +import com.google.cloud.storage.HmacKey.HmacKeyMetadata; +import com.google.cloud.storage.ServiceAccount; +import com.google.cloud.storage.conformance.retry.Functions.VoidFunction; +import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.Immutable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * A specialized wrapper around an immutable map allowing for declaration of named get/has/with + * methods without the need for individual fields. + * + *

Every mutation returns a copy with the mutation result. + * + *

Over the course of executing an individual test for a specific mapping some fields will be + * updated. + * + *

This approach was taken after multiple attempts to create more type safe alternatives which + * turned into far too much duplication given the possible permutations of state for various + * mappings. + */ +@Immutable +final class State { + + private static final State EMPTY = new State(); + private static final Key KEY_ACL = new Key<>("acl"); + private static final Key KEY_BLOB = new Key<>("blob"); + private static final Key KEY_BLOB_ID = new Key<>("blobId"); + private static final Key KEY_COPY_DEST = new Key<>("copyDest"); + private static final Key KEY_BLOB_INFO = new Key<>("blobInfo"); + private static final Key KEY_BOOL = new Key<>("bool"); + private static final Key KEY_BUCKET = new Key<>("bucket"); + private static final Key KEY_BUCKET_INFO = new Key<>("bucketInfo"); + private static final Key KEY_COPY = new Key<>("copy"); + private static final Key KEY_HMAC_KEY = new Key<>("hmacKey"); + private static final Key KEY_HMAC_KEY_METADATA = new Key<>("hmacKeyMetadata"); + private static final Key KEY_POLICY = new Key<>("policy"); + private static final Key KEY_SERVICE_ACCOUNT = new Key<>("serviceAccount"); + private static final Key> KEY_LIST_OBJECTS = new Key<>("list"); + private static final Key> KEY_TEST_IAM_PERMISSIONS_RESULTS = + new Key<>("testIamPermissionsResults"); + private static final Key> KEY_ACLS = new Key<>("acls"); + private static final Key KEY_BYTES = new Key<>("bytes"); + + private final ImmutableMap, Object> data; + + public State() { + this(ImmutableMap.of()); + } + + public State(ImmutableMap, Object> data) { + this.data = data; + } + + static State empty() { + return EMPTY; + } + + public State consume(VoidFunction f) throws Throwable { + f.apply(); + return this; + } + + public boolean hasAcl() { + return hasValue(KEY_ACL); + } + + public Acl getAcl() { + return getValue(KEY_ACL); + } + + public State with(Acl acl) { + return newStateWith(KEY_ACL, acl); + } + + public boolean hasBlob() { + return hasValue(KEY_BLOB); + } + + public Blob getBlob() { + return getValue(KEY_BLOB); + } + + public State with(Blob blob) { + return newStateWith(KEY_BLOB, blob); + } + + public boolean hasBlobId() { + return hasValue(KEY_BLOB_ID); + } + + public BlobId getBlobId() { + return getValue(KEY_BLOB_ID); + } + + public State with(BlobId blobId) { + return newStateWith(KEY_BLOB_ID, blobId); + } + + public boolean hasCopyDest() { + return hasValue(KEY_COPY_DEST); + } + + public BlobId getCopyDest() { + return getValue(KEY_COPY_DEST); + } + + public State withCopyDest(BlobId copyDest) { + return newStateWith(KEY_COPY_DEST, copyDest); + } + + public boolean hasBlobInfo() { + return hasValue(KEY_BLOB_INFO); + } + + public BlobInfo getBlobInfo() { + return getValue(KEY_BLOB_INFO); + } + + public State with(BlobInfo blobInfo) { + return newStateWith(KEY_BLOB_INFO, blobInfo); + } + + public boolean hasBool() { + return hasValue(KEY_BOOL); + } + + public Boolean getBool() { + return getValue(KEY_BOOL); + } + + public State with(Boolean bool) { + return newStateWith(KEY_BOOL, bool); + } + + public boolean hasBucket() { + return hasValue(KEY_BUCKET); + } + + public Bucket getBucket() { + return getValue(KEY_BUCKET); + } + + public State with(Bucket bucket) { + return newStateWith(KEY_BUCKET, bucket); + } + + public boolean hasBucketInfo() { + return hasValue(KEY_BUCKET_INFO); + } + + public BucketInfo getBucketInfo() { + return getValue(KEY_BUCKET_INFO); + } + + public State with(BucketInfo bucketInfo) { + return newStateWith(KEY_BUCKET_INFO, bucketInfo); + } + + public boolean hasCopy() { + return hasValue(KEY_COPY); + } + + public CopyWriter getCopy() { + return getValue(KEY_COPY); + } + + public State with(CopyWriter copy) { + return newStateWith(KEY_COPY, copy); + } + + public boolean hasHmacKey() { + return hasValue(KEY_HMAC_KEY); + } + + public HmacKey getHmacKey() { + return getValue(KEY_HMAC_KEY); + } + + public State with(HmacKey hmacKey) { + return newStateWith(KEY_HMAC_KEY, hmacKey); + } + + public boolean hasHmacKeyMetadata() { + return hasValue(KEY_HMAC_KEY_METADATA); + } + + public HmacKeyMetadata getHmacKeyMetadata() { + return getValue(KEY_HMAC_KEY_METADATA); + } + + public State with(HmacKeyMetadata hmacKeyMetadata) { + return newStateWith(KEY_HMAC_KEY_METADATA, hmacKeyMetadata); + } + + public boolean hasPolicy() { + return hasValue(KEY_POLICY); + } + + public Policy getPolicy() { + return getValue(KEY_POLICY); + } + + public State with(Policy policy) { + return newStateWith(KEY_POLICY, policy); + } + + public boolean hasServiceAccount() { + return hasValue(KEY_SERVICE_ACCOUNT); + } + + public ServiceAccount getServiceAccount() { + return getValue(KEY_SERVICE_ACCOUNT); + } + + public State with(ServiceAccount serviceAccount) { + return newStateWith(KEY_SERVICE_ACCOUNT, serviceAccount); + } + + public boolean hasBytes() { + return hasValue(KEY_BYTES); + } + + public byte[] getBytes() { + return getValue(KEY_BYTES); + } + + public State with(byte[] bytes) { + return newStateWith(KEY_BYTES, bytes); + } + + public boolean hasTestIamPermissionsResults() { + return hasValue(KEY_TEST_IAM_PERMISSIONS_RESULTS); + } + + public List getTestIamPermissionsResults() { + return getValue(KEY_TEST_IAM_PERMISSIONS_RESULTS); + } + + public State withTestIamPermissionsResults(List testIamPermissionsResults) { + return newStateWith(KEY_TEST_IAM_PERMISSIONS_RESULTS, testIamPermissionsResults); + } + + public boolean hasAcls() { + return hasValue(KEY_ACLS); + } + + public List getAcls() { + return getValue(KEY_ACLS); + } + + public State withAcls(List acls) { + return newStateWith(KEY_ACLS, acls); + } + + public State consume(Page page) { + List collect = + StreamSupport.stream(page.iterateAll().spliterator(), false).collect(Collectors.toList()); + return newStateWith(KEY_LIST_OBJECTS, collect); + } + + private T getValue(Key key) { + Object o = data.get(key); + requireNonNull(o, () -> String.format("%s was not found in state", key.name)); + return key.cast(o); + } + + private boolean hasValue(Key key) { + return data.containsKey(key); + } + + private State newStateWith(Key key, T t) { + requireNonNull(t, () -> String.format("null value provided for %s", key.name)); + Map, Object> tmp = new HashMap<>(data); + tmp.put(key, t); + return new State(ImmutableMap.copyOf(tmp)); + } + + private static final class Key { + + private final String name; + + public Key(String name) { + this.name = requireNonNull(name, "name must be non null"); + } + + T cast(Object t) { + return (T) t; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Key)) { + return false; + } + Key key = (Key) o; + return name.equals(key.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java new file mode 100644 index 0000000000..f344b98041 --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java @@ -0,0 +1,343 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static com.google.cloud.RetryHelper.runWithRetries; +import static java.util.Objects.requireNonNull; + +import com.google.api.client.http.ByteArrayContent; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.core.NanoClock; +import com.google.api.gax.retrying.BasicResultRetryAlgorithm; +import com.google.api.gax.retrying.RetrySettings; +import com.google.cloud.RetryHelper.RetryHelperException; +import com.google.common.collect.ImmutableList; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.SocketException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.threeten.bp.Duration; + +/** + * A JUnit 4 {@link TestRule} which integrates with the storage-testbench by pulling the + * docker image, starting the container, providing methods for interacting with the {@code + * /retry_test} rest api, stopping the container. + * + *

This rule expects to be bound as an {@link org.junit.ClassRule @ClassRule} field. + */ +final class TestBench implements TestRule { + + private static final Logger LOGGER = Logger.getLogger(TestBench.class.getName()); + + private final boolean ignorePullError; + private final String baseUri; + private final String dockerImageName; + private final String dockerImageTag; + private final CleanupStrategy cleanupStrategy; + + private final Gson gson; + private final HttpRequestFactory requestFactory; + + private TestBench( + boolean ignorePullError, + String baseUri, + String dockerImageName, + String dockerImageTag, + CleanupStrategy cleanupStrategy) { + this.ignorePullError = ignorePullError; + this.baseUri = baseUri; + this.dockerImageName = dockerImageName; + this.dockerImageTag = dockerImageTag; + this.cleanupStrategy = cleanupStrategy; + this.gson = new Gson(); + this.requestFactory = + new NetHttpTransport.Builder() + .build() + .createRequestFactory( + request -> { + request.setCurlLoggingEnabled(false); + request.getHeaders().setAccept("application/json"); + request.getHeaders().setUserAgent("java-conformance-tests/"); + }); + } + + String getBaseUri() { + return baseUri; + } + + RetryTestResource createRetryTest(RetryTestResource retryTestResource) throws IOException { + GenericUrl url = new GenericUrl(baseUri + "/retry_test"); + String jsonString = gson.toJson(retryTestResource); + HttpContent content = + new ByteArrayContent("application/json", jsonString.getBytes(StandardCharsets.UTF_8)); + HttpRequest req = requestFactory.buildPostRequest(url, content); + HttpResponse resp = req.execute(); + RetryTestResource result = gson.fromJson(resp.parseAsString(), RetryTestResource.class); + resp.disconnect(); + return result; + } + + void deleteRetryTest(RetryTestResource retryTestResource) throws IOException { + GenericUrl url = new GenericUrl(baseUri + "/retry_test/" + retryTestResource.id); + HttpRequest req = requestFactory.buildDeleteRequest(url); + HttpResponse resp = req.execute(); + resp.disconnect(); + } + + RetryTestResource getRetryTest(RetryTestResource retryTestResource) throws IOException { + GenericUrl url = new GenericUrl(baseUri + "/retry_test/" + retryTestResource.id); + HttpRequest req = requestFactory.buildGetRequest(url); + HttpResponse resp = req.execute(); + RetryTestResource result = gson.fromJson(resp.parseAsString(), RetryTestResource.class); + resp.disconnect(); + return result; + } + + List listRetryTests() throws IOException { + GenericUrl url = new GenericUrl(baseUri + "/retry_tests"); + HttpRequest req = requestFactory.buildGetRequest(url); + HttpResponse resp = req.execute(); + JsonObject result = gson.fromJson(resp.parseAsString(), JsonObject.class); + JsonArray retryTest = (JsonArray) result.get("retry_test"); + ImmutableList.Builder b = ImmutableList.builder(); + for (JsonElement e : retryTest) { + b.add(gson.fromJson(e, RetryTestResource.class)); + } + resp.disconnect(); + return b.build(); + } + + @Override + public Statement apply(final Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Path tempDirectory = Files.createTempDirectory("retry-conformance-server"); + File outFile = tempDirectory.resolve("stdout").toFile(); + File errFile = tempDirectory.resolve("stderr").toFile(); + LOGGER.info("Redirecting server stdout to: " + outFile.getAbsolutePath()); + LOGGER.info("Redirecting server stderr to: " + errFile.getAbsolutePath()); + String dockerImage = String.format("%s:%s", dockerImageName, dockerImageTag); + // First try and pull the docker image, this validates docker is available and running + // on the host, as well as gives time for the image to be downloaded independently of + // trying to start the container. (Below, when we first start the container we then attempt + // to issue a call against the api before we yield to run our tests.) + try { + Process p = + new ProcessBuilder() + .command("docker", "pull", dockerImage) + .redirectOutput(outFile) + .redirectError(errFile) + .start(); + p.waitFor(5, TimeUnit.MINUTES); + if (!ignorePullError && p.exitValue() != 0) { + dumpServerLogs(outFile, errFile); + throw new IllegalStateException( + String.format( + "Non-zero status while attempting to pull docker image '%s'", dockerImage)); + } + } catch (InterruptedException | IllegalThreadStateException e) { + dumpServerLogs(outFile, errFile); + throw new IllegalStateException( + String.format("Timeout while attempting to pull docker image '%s'", dockerImage)); + } + + int port = URI.create(baseUri).getPort(); + final Process process = + new ProcessBuilder() + .command( + "docker", + "run", + "-i", + "--rm", + "--publish", + port + ":9000", + "--name=retry-conformance-server", + dockerImage) + .redirectOutput(outFile) + .redirectError(errFile) + .start(); + boolean success = false; + try { + // wait a small amount of time for the server to come up before probing + Thread.sleep(500); + // wait for the server to come up + List existingResources = + runWithRetries( + TestBench.this::listRetryTests, + RetrySettings.newBuilder() + .setTotalTimeout(Duration.ofSeconds(30)) + .setInitialRetryDelay(Duration.ofMillis(500)) + .setRetryDelayMultiplier(1.5) + .setMaxRetryDelay(Duration.ofSeconds(5)) + .build(), + new BasicResultRetryAlgorithm>() { + @Override + public boolean shouldRetry( + Throwable previousThrowable, List previousResponse) { + return previousThrowable instanceof SocketException; + } + }, + NanoClock.getDefaultClock()); + if (!existingResources.isEmpty()) { + LOGGER.info( + "Test Server already has retry tests in it, is it running outside the tests?"); + } + base.evaluate(); + success = true; + } catch (RetryHelperException e) { + dumpServerLogs(outFile, errFile); + throw new IllegalStateException( + "Failed to connect to server within a reasonable amount of time. Host url: " + + baseUri, + e.getCause()); + } finally { + process.destroy(); + if (cleanupStrategy == CleanupStrategy.ALWAYS + || (success && cleanupStrategy == CleanupStrategy.ONLY_ON_SUCCESS)) { + outFile.delete(); + errFile.delete(); + Files.delete(tempDirectory); + } + } + } + }; + } + + private void dumpServerLogs(File outFile, File errFile) throws IOException { + try { + LOGGER.warning("Dumping contents of stdout"); + dumpServerLog("stdout", outFile); + } finally { + LOGGER.warning("Dumping contents of stderr"); + dumpServerLog("stderr", errFile); + } + } + + private void dumpServerLog(String prefix, File out) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(out))) { + String line; + while ((line = reader.readLine()) != null) { + LOGGER.warning("<" + prefix + "> " + line); + } + } + } + + static Builder newBuilder() { + return new Builder(); + } + + static final class RetryTestResource { + public String id; + public Boolean completed; + public JsonObject instructions; + + @Override + public String toString() { + return "RetryTestResource{" + + "id='" + + id + + '\'' + + ", completed=" + + completed + + ", instructions=" + + instructions + + '}'; + } + } + + static final class Builder { + private static final String DEFAULT_BASE_URI = "http://localhost:9000"; + private static final String DEFAULT_IMAGE_NAME = + "gcr.io/cloud-devrel-public-resources/storage-testbench"; + private static final String DEFAULT_IMAGE_TAG = "latest"; + + private boolean ignorePullError; + private String baseUri; + private String dockerImageName; + private String dockerImageTag; + private CleanupStrategy cleanupStrategy; + + public Builder() { + this(false, DEFAULT_BASE_URI, DEFAULT_IMAGE_NAME, DEFAULT_IMAGE_TAG, CleanupStrategy.ALWAYS); + } + + public Builder( + boolean ignorePullError, + String baseUri, + String dockerImageName, + String dockerImageTag, + CleanupStrategy cleanupStrategy) { + this.ignorePullError = ignorePullError; + this.baseUri = baseUri; + this.dockerImageName = dockerImageName; + this.dockerImageTag = dockerImageTag; + this.cleanupStrategy = cleanupStrategy; + } + + public Builder setCleanupStraegy(CleanupStrategy cleanupStrategy) { + this.cleanupStrategy = requireNonNull(cleanupStrategy, "cleanupStrategy must be non null"); + return this; + } + + public Builder setIgnorePullError(boolean ignorePullError) { + this.ignorePullError = ignorePullError; + return this; + } + + public Builder setBaseUri(String baseUri) { + this.baseUri = requireNonNull(baseUri, "host must be non null"); + return this; + } + + public Builder setDockerImageName(String dockerImageName) { + this.dockerImageName = requireNonNull(dockerImageName, "dockerImageName must be non null"); + return this; + } + + public Builder setDockerImageTag(String dockerImageTag) { + this.dockerImageTag = requireNonNull(dockerImageTag, "dockerImageTag must be non null"); + return this; + } + + public TestBench build() { + return new TestBench( + ignorePullError, baseUri, dockerImageName, dockerImageTag, cleanupStrategy); + } + } +} diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestRetryConformance.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestRetryConformance.java new file mode 100644 index 0000000000..953cb5998a --- /dev/null +++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestRetryConformance.java @@ -0,0 +1,210 @@ +/* + * Copyright 2021 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.storage.conformance.retry; + +import static java.util.Objects.requireNonNull; +import static org.junit.Assert.assertNotNull; + +import com.google.auth.ServiceAccountSigner; +import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.conformance.storage.v1.InstructionList; +import com.google.cloud.conformance.storage.v1.Method; +import com.google.common.base.Joiner; +import com.google.errorprone.annotations.Immutable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.stream.Collectors; + +/** + * An individual resolved test case correlating config from {@link + * com.google.cloud.conformance.storage.v1.RetryTest}s: the specific rpc method being tested, the + * instructions and the corresponding mapping id. + * + *

Generates some unique values for use in parallel test execution such as bucket names, object + * names, etc. + */ +@Immutable +final class TestRetryConformance { + static final String BASE_ID; + + static { + Instant now = Clock.systemUTC().instant(); + DateTimeFormatter formatter = + DateTimeFormatter.ISO_LOCAL_TIME.withZone(ZoneId.from(ZoneOffset.UTC)); + BASE_ID = formatter.format(now).replaceAll("[:]", "").substring(0, 6); + } + + private final String bucketName; + private final String bucketName2; + private final String userProject; + private final String objectName; + + private final byte[] helloWorldUtf8Bytes = "Hello, World!!!".getBytes(StandardCharsets.UTF_8); + private final Path helloWorldFilePath = resolvePathForResource(); + private final ServiceAccountCredentials serviceAccountCredentials = + resolveServiceAccountCredentials(); + + private final String host; + + private final int scenarioId; + private final Method method; + private final InstructionList instruction; + private final boolean preconditionsProvided; + private final boolean expectSuccess; + private final int mappingId; + + TestRetryConformance( + String host, + int scenarioId, + Method method, + InstructionList instruction, + boolean preconditionsProvided, + boolean expectSuccess) { + this(host, scenarioId, method, instruction, preconditionsProvided, expectSuccess, 0); + } + + TestRetryConformance( + String host, + int scenarioId, + Method method, + InstructionList instruction, + boolean preconditionsProvided, + boolean expectSuccess, + int mappingId) { + this.host = host; + this.scenarioId = scenarioId; + this.method = requireNonNull(method, "method must be non null"); + this.instruction = requireNonNull(instruction, "instruction must be non null"); + this.preconditionsProvided = preconditionsProvided; + this.expectSuccess = expectSuccess; + this.mappingId = mappingId; + String instructionsString = + this.instruction.getInstructionsList().stream() + .map(s -> s.replace("return-", "")) + .collect(Collectors.joining("_")); + this.bucketName = + String.format("%s_s%03d-%s-m%03d_bkt1", BASE_ID, scenarioId, instructionsString, mappingId); + this.bucketName2 = + String.format("%s_s%03d-%s-m%03d_bkt2", BASE_ID, scenarioId, instructionsString, mappingId); + this.userProject = + String.format("%s_s%03d-%s-m%03d_prj1", BASE_ID, scenarioId, instructionsString, mappingId); + this.objectName = + String.format("%s_s%03d-%s-m%03d_obj1", BASE_ID, scenarioId, instructionsString, mappingId); + } + + public String getHost() { + return host; + } + + public String getBucketName() { + return bucketName; + } + + public String getBucketName2() { + return bucketName2; + } + + public String getUserProject() { + return userProject; + } + + public String getObjectName() { + return objectName; + } + + public byte[] getHelloWorldUtf8Bytes() { + return helloWorldUtf8Bytes; + } + + public Path getHelloWorldFilePath() { + return helloWorldFilePath; + } + + public int getScenarioId() { + return scenarioId; + } + + public Method getMethod() { + return method; + } + + public InstructionList getInstruction() { + return instruction; + } + + public boolean isPreconditionsProvided() { + return preconditionsProvided; + } + + public boolean isExpectSuccess() { + return expectSuccess; + } + + public int getMappingId() { + return mappingId; + } + + public ServiceAccountSigner getServiceAccountSigner() { + return serviceAccountCredentials; + } + + public String getTestName() { + String instructionsDesc = Joiner.on("_").join(instruction.getInstructionsList()); + return String.format( + "TestRetryConformance/%d-[%s]-%s-%d", + scenarioId, instructionsDesc, method.getName(), mappingId); + } + + @Override + public String toString() { + return getTestName(); + } + + private static Path resolvePathForResource() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + URL url = cl.getResource("com/google/cloud/storage/conformance/retry/hello-world.txt"); + assertNotNull(url); + try { + return Paths.get(url.toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + private static ServiceAccountCredentials resolveServiceAccountCredentials() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + InputStream inputStream = + cl.getResourceAsStream( + "com/google/cloud/conformance/storage/v1/test_service_account.not-a-test.json"); + assertNotNull(inputStream); + try { + return ServiceAccountCredentials.fromStream(inputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/google-cloud-storage/src/test/resources/com/google/cloud/storage/conformance/retry/hello-world.txt b/google-cloud-storage/src/test/resources/com/google/cloud/storage/conformance/retry/hello-world.txt new file mode 100644 index 0000000000..5ac001b76f --- /dev/null +++ b/google-cloud-storage/src/test/resources/com/google/cloud/storage/conformance/retry/hello-world.txt @@ -0,0 +1 @@ +Hello, World!!! diff --git a/google-cloud-storage/src/test/resources/com/google/cloud/storage/conformance/retry/testNamesWhichShouldSucceed.txt b/google-cloud-storage/src/test/resources/com/google/cloud/storage/conformance/retry/testNamesWhichShouldSucceed.txt new file mode 100644 index 0000000000..c8f6606b59 --- /dev/null +++ b/google-cloud-storage/src/test/resources/com/google/cloud/storage/conformance/retry/testNamesWhichShouldSucceed.txt @@ -0,0 +1,322 @@ +# Each line should be a full test name +# Each test name present in the file will be expected to pass in a CI environment +# +# This list can be regenerated by running the following command in a shell: +# mvn test && xq '//testsuite/testcase[not(./error) and not(./failure) and not(./skipped)]/@name' google-cloud-storage/target/surefire-reports/TEST-com.google.cloud.storage.conformance.retry.ITRetryConformanceTest-sponge_log.xml | tail -n+2 | head -n-1 | sed 's# test\[##g' | sed 's#\]##g' | sort | tee -a google-cloud-storage/src/test/resources/com/google/cloud/storage/conformance/retry/testNamesWhichShouldSucceed.txt +# where xq is the package from https://github.com/jeffbr13/xq + +TestRetryConformance/1-[return-503_return-503]-storage.bucket_acl.list-7 +TestRetryConformance/1-[return-503_return-503]-storage.bucket_acl.list-8 +TestRetryConformance/1-[return-503_return-503]-storage.bucket_acl.list-90 +TestRetryConformance/1-[return-503_return-503]-storage.buckets.delete-11 +TestRetryConformance/1-[return-503_return-503]-storage.buckets.delete-92 +TestRetryConformance/1-[return-503_return-503]-storage.buckets.get-12 +TestRetryConformance/1-[return-503_return-503]-storage.buckets.get-96 +TestRetryConformance/1-[return-503_return-503]-storage.buckets.getIamPolicy-13 +TestRetryConformance/1-[return-503_return-503]-storage.buckets.insert-14 +TestRetryConformance/1-[return-503_return-503]-storage.buckets.lockRetentionPolicy-100 +TestRetryConformance/1-[return-503_return-503]-storage.buckets.testIamPermissions-19 +TestRetryConformance/1-[return-503_return-503]-storage.default_object_acl.list-105 +TestRetryConformance/1-[return-503_return-503]-storage.default_object_acl.list-23 +TestRetryConformance/1-[return-503_return-503]-storage.hmacKey.get-27 +TestRetryConformance/1-[return-503_return-503]-storage.hmacKey.list-28 +TestRetryConformance/1-[return-503_return-503]-storage.object_acl.list-33 +TestRetryConformance/1-[return-503_return-503]-storage.object_acl.list-65 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-107 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-39 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-40 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-41 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-42 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-43 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-44 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-45 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-75 +TestRetryConformance/1-[return-503_return-503]-storage.objects.get-76 +TestRetryConformance/1-[return-503_return-503]-storage.objects.list-55 +TestRetryConformance/1-[return-503_return-503]-storage.serviceaccount.get-59 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.bucket_acl.list-7 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.bucket_acl.list-8 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.bucket_acl.list-90 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.buckets.delete-11 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.buckets.delete-92 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.buckets.get-12 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.buckets.get-96 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.buckets.getIamPolicy-13 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.buckets.insert-14 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.buckets.lockRetentionPolicy-100 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.buckets.testIamPermissions-19 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.default_object_acl.list-105 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.default_object_acl.list-23 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.hmacKey.get-27 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.hmacKey.list-28 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.object_acl.list-33 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.object_acl.list-65 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-107 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-39 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-40 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-41 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-42 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-43 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-44 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-45 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-75 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.get-76 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.objects.list-55 +TestRetryConformance/1-[return-reset-connection_return-503]-storage.serviceaccount.get-59 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.bucket_acl.list-7 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.bucket_acl.list-8 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.bucket_acl.list-90 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.buckets.delete-11 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.buckets.delete-92 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.buckets.get-12 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.buckets.get-96 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.buckets.getIamPolicy-13 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.buckets.insert-14 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.buckets.lockRetentionPolicy-100 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.buckets.testIamPermissions-19 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.default_object_acl.list-105 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.default_object_acl.list-23 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.hmacKey.get-27 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.hmacKey.list-28 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.object_acl.list-33 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.object_acl.list-65 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-107 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-39 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-40 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-41 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-42 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-43 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-44 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-45 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-75 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.get-76 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.objects.list-55 +TestRetryConformance/1-[return-reset-connection_return-reset-connection]-storage.serviceaccount.get-59 +TestRetryConformance/2-[return-503_return-503]-storage.buckets.patch-101 +TestRetryConformance/2-[return-503_return-503]-storage.hmacKey.update-29 +TestRetryConformance/2-[return-503_return-503]-storage.objects.delete-37 +TestRetryConformance/2-[return-503_return-503]-storage.objects.delete-38 +TestRetryConformance/2-[return-503_return-503]-storage.objects.delete-67 +TestRetryConformance/2-[return-503_return-503]-storage.objects.delete-68 +TestRetryConformance/2-[return-503_return-503]-storage.objects.insert-46 +TestRetryConformance/2-[return-503_return-503]-storage.objects.insert-47 +TestRetryConformance/2-[return-503_return-503]-storage.objects.patch-56 +TestRetryConformance/2-[return-503_return-503]-storage.objects.patch-57 +TestRetryConformance/2-[return-503_return-503]-storage.objects.patch-79 +TestRetryConformance/2-[return-503_return-503]-storage.objects.patch-80 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.buckets.patch-101 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.hmacKey.update-29 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.delete-37 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.delete-38 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.delete-67 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.delete-68 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.insert-46 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.insert-47 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.patch-56 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.patch-57 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.patch-79 +TestRetryConformance/2-[return-reset-connection_return-503]-storage.objects.patch-80 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.buckets.patch-101 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.hmacKey.update-29 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.delete-37 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.delete-38 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.delete-67 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.delete-68 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.insert-46 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.insert-47 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.patch-56 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.patch-57 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.patch-79 +TestRetryConformance/2-[return-reset-connection_return-reset-connection]-storage.objects.patch-80 +TestRetryConformance/3-[return-503]-storage.objects.insert-110 +TestRetryConformance/3-[return-503]-storage.objects.insert-111 +TestRetryConformance/3-[return-503]-storage.objects.insert-114 +TestRetryConformance/3-[return-reset-connection]-storage.objects.insert-110 +TestRetryConformance/3-[return-reset-connection]-storage.objects.insert-111 +TestRetryConformance/3-[return-reset-connection]-storage.objects.insert-114 +TestRetryConformance/5-[return-400]-storage.bucket_acl.delete-1 +TestRetryConformance/5-[return-400]-storage.bucket_acl.delete-2 +TestRetryConformance/5-[return-400]-storage.bucket_acl.delete-87 +TestRetryConformance/5-[return-400]-storage.bucket_acl.get-3 +TestRetryConformance/5-[return-400]-storage.bucket_acl.get-4 +TestRetryConformance/5-[return-400]-storage.bucket_acl.get-88 +TestRetryConformance/5-[return-400]-storage.bucket_acl.insert-5 +TestRetryConformance/5-[return-400]-storage.bucket_acl.insert-6 +TestRetryConformance/5-[return-400]-storage.bucket_acl.insert-89 +TestRetryConformance/5-[return-400]-storage.bucket_acl.list-7 +TestRetryConformance/5-[return-400]-storage.bucket_acl.list-8 +TestRetryConformance/5-[return-400]-storage.bucket_acl.list-90 +TestRetryConformance/5-[return-400]-storage.bucket_acl.patch-10 +TestRetryConformance/5-[return-400]-storage.bucket_acl.patch-9 +TestRetryConformance/5-[return-400]-storage.bucket_acl.patch-91 +TestRetryConformance/5-[return-400]-storage.buckets.delete-11 +TestRetryConformance/5-[return-400]-storage.buckets.delete-92 +TestRetryConformance/5-[return-400]-storage.buckets.get-12 +TestRetryConformance/5-[return-400]-storage.buckets.get-94 +TestRetryConformance/5-[return-400]-storage.buckets.get-96 +TestRetryConformance/5-[return-400]-storage.buckets.getIamPolicy-13 +TestRetryConformance/5-[return-400]-storage.buckets.insert-14 +TestRetryConformance/5-[return-400]-storage.buckets.lockRetentionPolicy-100 +TestRetryConformance/5-[return-400]-storage.buckets.patch-101 +TestRetryConformance/5-[return-400]-storage.buckets.patch-17 +TestRetryConformance/5-[return-400]-storage.buckets.setIamPolicy-18 +TestRetryConformance/5-[return-400]-storage.buckets.testIamPermissions-19 +TestRetryConformance/5-[return-400]-storage.default_object_acl.delete-102 +TestRetryConformance/5-[return-400]-storage.default_object_acl.delete-20 +TestRetryConformance/5-[return-400]-storage.default_object_acl.get-103 +TestRetryConformance/5-[return-400]-storage.default_object_acl.get-21 +TestRetryConformance/5-[return-400]-storage.default_object_acl.insert-104 +TestRetryConformance/5-[return-400]-storage.default_object_acl.insert-22 +TestRetryConformance/5-[return-400]-storage.default_object_acl.list-105 +TestRetryConformance/5-[return-400]-storage.default_object_acl.list-23 +TestRetryConformance/5-[return-400]-storage.default_object_acl.patch-106 +TestRetryConformance/5-[return-400]-storage.default_object_acl.patch-24 +TestRetryConformance/5-[return-400]-storage.hmacKey.delete-26 +TestRetryConformance/5-[return-400]-storage.hmacKey.get-27 +TestRetryConformance/5-[return-400]-storage.hmacKey.list-28 +TestRetryConformance/5-[return-400]-storage.hmacKey.update-29 +TestRetryConformance/5-[return-400]-storage.object_acl.delete-30 +TestRetryConformance/5-[return-400]-storage.object_acl.delete-62 +TestRetryConformance/5-[return-400]-storage.object_acl.get-31 +TestRetryConformance/5-[return-400]-storage.object_acl.get-63 +TestRetryConformance/5-[return-400]-storage.object_acl.insert-32 +TestRetryConformance/5-[return-400]-storage.object_acl.insert-64 +TestRetryConformance/5-[return-400]-storage.object_acl.list-33 +TestRetryConformance/5-[return-400]-storage.object_acl.list-65 +TestRetryConformance/5-[return-400]-storage.object_acl.patch-34 +TestRetryConformance/5-[return-400]-storage.object_acl.patch-66 +TestRetryConformance/5-[return-400]-storage.objects.compose-35 +TestRetryConformance/5-[return-400]-storage.objects.delete-36 +TestRetryConformance/5-[return-400]-storage.objects.delete-67 +TestRetryConformance/5-[return-400]-storage.objects.delete-68 +TestRetryConformance/5-[return-400]-storage.objects.get-107 +TestRetryConformance/5-[return-400]-storage.objects.get-39 +TestRetryConformance/5-[return-400]-storage.objects.get-40 +TestRetryConformance/5-[return-400]-storage.objects.get-41 +TestRetryConformance/5-[return-400]-storage.objects.get-42 +TestRetryConformance/5-[return-400]-storage.objects.get-43 +TestRetryConformance/5-[return-400]-storage.objects.get-60 +TestRetryConformance/5-[return-400]-storage.objects.get-69 +TestRetryConformance/5-[return-400]-storage.objects.get-70 +TestRetryConformance/5-[return-400]-storage.objects.get-71 +TestRetryConformance/5-[return-400]-storage.objects.get-72 +TestRetryConformance/5-[return-400]-storage.objects.get-73 +TestRetryConformance/5-[return-400]-storage.objects.get-74 +TestRetryConformance/5-[return-400]-storage.objects.get-75 +TestRetryConformance/5-[return-400]-storage.objects.get-76 +TestRetryConformance/5-[return-400]-storage.objects.insert-108 +TestRetryConformance/5-[return-400]-storage.objects.insert-109 +TestRetryConformance/5-[return-400]-storage.objects.insert-110 +TestRetryConformance/5-[return-400]-storage.objects.insert-111 +TestRetryConformance/5-[return-400]-storage.objects.insert-112 +TestRetryConformance/5-[return-400]-storage.objects.insert-113 +TestRetryConformance/5-[return-400]-storage.objects.insert-114 +TestRetryConformance/5-[return-400]-storage.objects.insert-115 +TestRetryConformance/5-[return-400]-storage.objects.insert-116 +TestRetryConformance/5-[return-400]-storage.objects.insert-117 +TestRetryConformance/5-[return-400]-storage.objects.list-55 +TestRetryConformance/5-[return-400]-storage.objects.patch-56 +TestRetryConformance/5-[return-400]-storage.objects.patch-57 +TestRetryConformance/5-[return-400]-storage.objects.patch-79 +TestRetryConformance/5-[return-400]-storage.objects.patch-80 +TestRetryConformance/5-[return-400]-storage.objects.rewrite-58 +TestRetryConformance/5-[return-400]-storage.objects.rewrite-81 +TestRetryConformance/5-[return-400]-storage.objects.rewrite-82 +TestRetryConformance/5-[return-400]-storage.objects.rewrite-83 +TestRetryConformance/5-[return-400]-storage.objects.rewrite-84 +TestRetryConformance/5-[return-400]-storage.objects.rewrite-85 +TestRetryConformance/5-[return-400]-storage.objects.rewrite-86 +TestRetryConformance/5-[return-400]-storage.serviceaccount.get-59 +TestRetryConformance/5-[return-401]-storage.bucket_acl.delete-1 +TestRetryConformance/5-[return-401]-storage.bucket_acl.delete-2 +TestRetryConformance/5-[return-401]-storage.bucket_acl.delete-87 +TestRetryConformance/5-[return-401]-storage.bucket_acl.get-3 +TestRetryConformance/5-[return-401]-storage.bucket_acl.get-4 +TestRetryConformance/5-[return-401]-storage.bucket_acl.get-88 +TestRetryConformance/5-[return-401]-storage.bucket_acl.insert-5 +TestRetryConformance/5-[return-401]-storage.bucket_acl.insert-6 +TestRetryConformance/5-[return-401]-storage.bucket_acl.insert-89 +TestRetryConformance/5-[return-401]-storage.bucket_acl.list-7 +TestRetryConformance/5-[return-401]-storage.bucket_acl.list-8 +TestRetryConformance/5-[return-401]-storage.bucket_acl.list-90 +TestRetryConformance/5-[return-401]-storage.bucket_acl.patch-10 +TestRetryConformance/5-[return-401]-storage.bucket_acl.patch-9 +TestRetryConformance/5-[return-401]-storage.bucket_acl.patch-91 +TestRetryConformance/5-[return-401]-storage.buckets.delete-11 +TestRetryConformance/5-[return-401]-storage.buckets.delete-92 +TestRetryConformance/5-[return-401]-storage.buckets.get-12 +TestRetryConformance/5-[return-401]-storage.buckets.get-94 +TestRetryConformance/5-[return-401]-storage.buckets.get-96 +TestRetryConformance/5-[return-401]-storage.buckets.getIamPolicy-13 +TestRetryConformance/5-[return-401]-storage.buckets.insert-14 +TestRetryConformance/5-[return-401]-storage.buckets.lockRetentionPolicy-100 +TestRetryConformance/5-[return-401]-storage.buckets.patch-101 +TestRetryConformance/5-[return-401]-storage.buckets.patch-17 +TestRetryConformance/5-[return-401]-storage.buckets.setIamPolicy-18 +TestRetryConformance/5-[return-401]-storage.buckets.testIamPermissions-19 +TestRetryConformance/5-[return-401]-storage.default_object_acl.delete-102 +TestRetryConformance/5-[return-401]-storage.default_object_acl.delete-20 +TestRetryConformance/5-[return-401]-storage.default_object_acl.get-103 +TestRetryConformance/5-[return-401]-storage.default_object_acl.get-21 +TestRetryConformance/5-[return-401]-storage.default_object_acl.insert-104 +TestRetryConformance/5-[return-401]-storage.default_object_acl.insert-22 +TestRetryConformance/5-[return-401]-storage.default_object_acl.list-105 +TestRetryConformance/5-[return-401]-storage.default_object_acl.list-23 +TestRetryConformance/5-[return-401]-storage.default_object_acl.patch-106 +TestRetryConformance/5-[return-401]-storage.default_object_acl.patch-24 +TestRetryConformance/5-[return-401]-storage.hmacKey.delete-26 +TestRetryConformance/5-[return-401]-storage.hmacKey.get-27 +TestRetryConformance/5-[return-401]-storage.hmacKey.list-28 +TestRetryConformance/5-[return-401]-storage.hmacKey.update-29 +TestRetryConformance/5-[return-401]-storage.object_acl.delete-30 +TestRetryConformance/5-[return-401]-storage.object_acl.delete-62 +TestRetryConformance/5-[return-401]-storage.object_acl.get-31 +TestRetryConformance/5-[return-401]-storage.object_acl.get-63 +TestRetryConformance/5-[return-401]-storage.object_acl.insert-32 +TestRetryConformance/5-[return-401]-storage.object_acl.insert-64 +TestRetryConformance/5-[return-401]-storage.object_acl.list-33 +TestRetryConformance/5-[return-401]-storage.object_acl.list-65 +TestRetryConformance/5-[return-401]-storage.object_acl.patch-34 +TestRetryConformance/5-[return-401]-storage.object_acl.patch-66 +TestRetryConformance/5-[return-401]-storage.objects.compose-35 +TestRetryConformance/5-[return-401]-storage.objects.delete-36 +TestRetryConformance/5-[return-401]-storage.objects.delete-67 +TestRetryConformance/5-[return-401]-storage.objects.delete-68 +TestRetryConformance/5-[return-401]-storage.objects.get-107 +TestRetryConformance/5-[return-401]-storage.objects.get-39 +TestRetryConformance/5-[return-401]-storage.objects.get-40 +TestRetryConformance/5-[return-401]-storage.objects.get-41 +TestRetryConformance/5-[return-401]-storage.objects.get-42 +TestRetryConformance/5-[return-401]-storage.objects.get-43 +TestRetryConformance/5-[return-401]-storage.objects.get-60 +TestRetryConformance/5-[return-401]-storage.objects.get-69 +TestRetryConformance/5-[return-401]-storage.objects.get-70 +TestRetryConformance/5-[return-401]-storage.objects.get-71 +TestRetryConformance/5-[return-401]-storage.objects.get-72 +TestRetryConformance/5-[return-401]-storage.objects.get-73 +TestRetryConformance/5-[return-401]-storage.objects.get-74 +TestRetryConformance/5-[return-401]-storage.objects.get-75 +TestRetryConformance/5-[return-401]-storage.objects.get-76 +TestRetryConformance/5-[return-401]-storage.objects.insert-108 +TestRetryConformance/5-[return-401]-storage.objects.insert-109 +TestRetryConformance/5-[return-401]-storage.objects.insert-110 +TestRetryConformance/5-[return-401]-storage.objects.insert-111 +TestRetryConformance/5-[return-401]-storage.objects.insert-112 +TestRetryConformance/5-[return-401]-storage.objects.insert-113 +TestRetryConformance/5-[return-401]-storage.objects.insert-114 +TestRetryConformance/5-[return-401]-storage.objects.insert-115 +TestRetryConformance/5-[return-401]-storage.objects.insert-116 +TestRetryConformance/5-[return-401]-storage.objects.insert-117 +TestRetryConformance/5-[return-401]-storage.objects.list-55 +TestRetryConformance/5-[return-401]-storage.objects.patch-56 +TestRetryConformance/5-[return-401]-storage.objects.patch-57 +TestRetryConformance/5-[return-401]-storage.objects.patch-79 +TestRetryConformance/5-[return-401]-storage.objects.patch-80 +TestRetryConformance/5-[return-401]-storage.objects.rewrite-58 +TestRetryConformance/5-[return-401]-storage.objects.rewrite-81 +TestRetryConformance/5-[return-401]-storage.objects.rewrite-82 +TestRetryConformance/5-[return-401]-storage.objects.rewrite-83 +TestRetryConformance/5-[return-401]-storage.objects.rewrite-84 +TestRetryConformance/5-[return-401]-storage.objects.rewrite-85 +TestRetryConformance/5-[return-401]-storage.objects.rewrite-86 +TestRetryConformance/5-[return-401]-storage.serviceaccount.get-59 diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml index 1aab710bde..39a1179eb8 100644 --- a/grpc-google-cloud-storage-v2/pom.xml +++ b/grpc-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-storage-v2 - 2.1.3-alpha + 2.1.4-alpha grpc-google-cloud-storage-v2 GRPC library for grpc-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.1.3 + 2.1.4 diff --git a/pom.xml b/pom.xml index 9868b859a3..f65c0c3c02 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-storage-parent pom - 2.1.3 + 2.1.4 Storage Parent https://github.com/googleapis/java-storage @@ -70,7 +70,7 @@ com.google.apis google-api-services-storage - v1-rev20210127-1.32.1 + v1-rev20210914-1.32.1 org.easymock @@ -93,17 +93,17 @@ com.google.api.grpc proto-google-cloud-storage-v2 - 2.1.3-alpha + 2.1.4-alpha com.google.api.grpc grpc-google-cloud-storage-v2 - 2.1.3-alpha + 2.1.4-alpha com.google.api.grpc gapic-google-cloud-storage-v2 - 2.1.3-alpha + 2.1.4-alpha com.google.cloud diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml index cbdc322af1..782b6637d1 100644 --- a/proto-google-cloud-storage-v2/pom.xml +++ b/proto-google-cloud-storage-v2/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-storage-v2 - 2.1.3-alpha + 2.1.4-alpha proto-google-cloud-storage-v2 PROTO library for proto-google-cloud-storage-v2 com.google.cloud google-cloud-storage-parent - 2.1.3 + 2.1.4 diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml index 712a7d61fc..0264e5a402 100644 --- a/samples/install-without-bom/pom.xml +++ b/samples/install-without-bom/pom.xml @@ -29,7 +29,7 @@ com.google.cloud google-cloud-storage - 2.1.2 + 2.1.3 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index cb4236a874..c920abbd3c 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -28,7 +28,7 @@ com.google.cloud google-cloud-storage - 2.1.2 + 2.1.3 diff --git a/versions.txt b/versions.txt index a4aeaaa6f5..8b44457b58 100644 --- a/versions.txt +++ b/versions.txt @@ -1,7 +1,7 @@ # Format: # module:released-version:current-version -google-cloud-storage:2.1.3:2.1.3 -gapic-google-cloud-storage-v2:2.1.3-alpha:2.1.3-alpha -grpc-google-cloud-storage-v2:2.1.3-alpha:2.1.3-alpha -proto-google-cloud-storage-v2:2.1.3-alpha:2.1.3-alpha +google-cloud-storage:2.1.4:2.1.4 +gapic-google-cloud-storage-v2:2.1.4-alpha:2.1.4-alpha +grpc-google-cloud-storage-v2:2.1.4-alpha:2.1.4-alpha +proto-google-cloud-storage-v2:2.1.4-alpha:2.1.4-alpha