From 32fdd606f392bc97dab7f37b1c566b3954839f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Wed, 17 Mar 2021 10:58:02 +0100 Subject: [PATCH] feat: add autoConfigEmulator connection option (#931) Adds `autoConfigEmulator` connection option. When this option is set to true for a connection: 1. The connection will by default try to connect to `localhost:9010` (unless a specific host/port is set in the connection URL). 2. Plain text communication will be enabled. 3. Authentication will be disabled. 4. The instance and database in the connection string will automatically be created on the emulator if any of them do not yet exist. Any existing instance and/or database will remain untouched. Towards https://github.com/googleapis/java-spanner-jdbc/issues/380 --- .../spanner/connection/ConnectionImpl.java | 3 + .../spanner/connection/ConnectionOptions.java | 55 +++- .../spanner/connection/EmulatorUtil.java | 85 +++++ .../connection/ConnectionOptionsTest.java | 31 ++ .../spanner/connection/EmulatorUtilTest.java | 294 ++++++++++++++++++ 5 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java index b2f8aeef08..4f9703807a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java @@ -210,6 +210,9 @@ static UnitOfWorkType of(TransactionMode transactionMode) { this.spannerPool = SpannerPool.INSTANCE; this.options = options; this.spanner = spannerPool.getSpanner(options, this); + if (options.isAutoConfigEmulator()) { + EmulatorUtil.maybeCreateInstanceAndDatabase(spanner, options.getDatabaseId()); + } this.dbClient = spanner.getDatabaseClient(options.getDatabaseId()); this.retryAbortsInternally = options.isRetryAbortsInternally(); this.readOnly = options.isReadOnly(); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java index ee8e05231f..46783e0f36 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionOptions.java @@ -161,6 +161,7 @@ public String[] getValidValues() { private static final String PLAIN_TEXT_PROTOCOL = "http:"; private static final String HOST_PROTOCOL = "https:"; private static final String DEFAULT_HOST = "https://spanner.googleapis.com"; + private static final String DEFAULT_EMULATOR_HOST = "http://localhost:9010"; /** Use plain text is only for local testing purposes. */ private static final String USE_PLAIN_TEXT_PROPERTY_NAME = "usePlainText"; /** Name of the 'autocommit' connection property. */ @@ -231,6 +232,10 @@ public String[] getValidValues() { OPTIMIZER_VERSION_PROPERTY_NAME, "Sets the default query optimizer version to use for this connection."), ConnectionProperty.createBooleanProperty("returnCommitStats", "", false), + ConnectionProperty.createBooleanProperty( + "autoConfigEmulator", + "Automatically configure the connection to try to connect to the Cloud Spanner emulator (true/false). The instance and database in the connection string will automatically be created if these do not yet exist on the emulator.", + false), ConnectionProperty.createBooleanProperty( LENIENT_PROPERTY_NAME, "Silently ignore unknown properties in the connection string/properties (true/false)", @@ -347,6 +352,14 @@ private boolean isValidUri(String uri) { *
  • retryAbortsInternally (boolean): Sets the initial retryAbortsInternally mode for the * connection. Default is true. *
  • optimizerVersion (string): Sets the query optimizer version to use for the connection. + *
  • autoConfigEmulator (boolean): Automatically configures the connection to connect to the + * Cloud Spanner emulator. If no host and port is specified in the connection string, the + * connection will automatically use the default emulator host/port combination + * (localhost:9010). Plain text communication will be enabled and authentication will be + * disabled. The instance and database in the connection string will automatically be + * created on the emulator if any of them do not yet exist. Any existing instance or + * database on the emulator will remain untouched. No other configuration is needed in + * order to connect to the emulator than setting this property. * * * @param uri The URI of the Spanner database to connect to. @@ -459,6 +472,7 @@ public static Builder newBuilder() { private final String userAgent; private final QueryOptions queryOptions; private final boolean returnCommitStats; + private final boolean autoConfigEmulator; private final boolean autocommit; private final boolean readOnly; @@ -483,18 +497,15 @@ private ConnectionOptions(Builder builder) { (builder.credentials == null && this.credentialsUrl == null) || this.oauthToken == null, "Cannot specify both credentials and an OAuth token."); - this.usePlainText = parseUsePlainText(this.uri); this.userAgent = parseUserAgent(this.uri); QueryOptions.Builder queryOptionsBuilder = QueryOptions.newBuilder(); queryOptionsBuilder.setOptimizerVersion(parseOptimizerVersion(this.uri)); this.queryOptions = queryOptionsBuilder.build(); this.returnCommitStats = parseReturnCommitStats(this.uri); + this.autoConfigEmulator = parseAutoConfigEmulator(this.uri); + this.usePlainText = this.autoConfigEmulator || parseUsePlainText(this.uri); + this.host = determineHost(matcher, autoConfigEmulator, usePlainText); - this.host = - matcher.group(Builder.HOST_GROUP) == null - ? DEFAULT_HOST - : (usePlainText ? PLAIN_TEXT_PROTOCOL : HOST_PROTOCOL) - + matcher.group(Builder.HOST_GROUP); this.instanceId = matcher.group(Builder.INSTANCE_GROUP); this.databaseName = matcher.group(Builder.DATABASE_GROUP); // Using credentials on a plain text connection is not allowed, so if the user has not specified @@ -549,6 +560,23 @@ private ConnectionOptions(Builder builder) { } } + private static String determineHost( + Matcher matcher, boolean autoConfigEmulator, boolean usePlainText) { + if (matcher.group(Builder.HOST_GROUP) == null) { + if (autoConfigEmulator) { + return DEFAULT_EMULATOR_HOST; + } else { + return DEFAULT_HOST; + } + } else { + if (usePlainText) { + return PLAIN_TEXT_PROTOCOL + matcher.group(Builder.HOST_GROUP); + } else { + return HOST_PROTOCOL + matcher.group(Builder.HOST_GROUP); + } + } + } + private static Integer parseIntegerProperty(String propertyName, String value) { if (value != null) { try { @@ -644,6 +672,11 @@ static boolean parseReturnCommitStats(String uri) { return value != null ? Boolean.valueOf(value) : false; } + static boolean parseAutoConfigEmulator(String uri) { + String value = parseUriProperty(uri, "autoConfigEmulator"); + return value != null ? Boolean.valueOf(value) : false; + } + @VisibleForTesting static boolean parseLenient(String uri) { String value = parseUriProperty(uri, LENIENT_PROPERTY_NAME); @@ -838,6 +871,16 @@ public boolean isReturnCommitStats() { return returnCommitStats; } + /** + * Whether connections created by this {@link ConnectionOptions} will automatically try to connect + * to the emulator using the default host/port of the emulator, and automatically create the + * instance and database that is specified in the connection string if these do not exist on the + * emulator instance. + */ + public boolean isAutoConfigEmulator() { + return autoConfigEmulator; + } + /** Interceptors that should be executed after each statement */ List getStatementExecutionInterceptors() { return statementExecutionInterceptors; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java new file mode 100644 index 0000000000..9ffa36c0a5 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/EmulatorUtil.java @@ -0,0 +1,85 @@ +/* + * 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.spanner.connection; + +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.InstanceConfigId; +import com.google.cloud.spanner.InstanceInfo; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.util.concurrent.ExecutionException; + +/** + * Util class for automatically generating a test instance and test database on a Cloud Spanner + * emulator instance. This makes it easier to automatically start a working emulator and test an + * application when working with JDBC. + */ +class EmulatorUtil { + + /** + * Creates the instance and the database that are specified in the connection string on the + * emulator that the given {@link Spanner} instance connects to if these do not already exist. + * + * @param spanner a {@link Spanner} instance that connects to an emulator instance + * @param databaseId the id of the instance and the database to create + */ + static void maybeCreateInstanceAndDatabase(Spanner spanner, DatabaseId databaseId) { + Preconditions.checkArgument( + NoCredentials.getInstance().equals(spanner.getOptions().getCredentials())); + try { + spanner + .getInstanceAdminClient() + .createInstance( + InstanceInfo.newBuilder(databaseId.getInstanceId()) + .setDisplayName("Automatically Generated Test Instance") + .setNodeCount(1) + .setInstanceConfigId( + InstanceConfigId.of( + databaseId.getInstanceId().getProject(), "emulator-config")) + .build()) + .get(); + } catch (ExecutionException executionException) { + SpannerException spannerException = (SpannerException) executionException.getCause(); + if (spannerException.getErrorCode() != ErrorCode.ALREADY_EXISTS) { + throw spannerException; + } + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + try { + spanner + .getDatabaseAdminClient() + .createDatabase( + databaseId.getInstanceId().getInstance(), + databaseId.getDatabase(), + ImmutableList.of()) + .get(); + } catch (ExecutionException executionException) { + SpannerException spannerException = (SpannerException) executionException.getCause(); + if (spannerException.getErrorCode() != ErrorCode.ALREADY_EXISTS) { + throw spannerException; + } + } catch (InterruptedException e) { + throw SpannerExceptionFactory.propagateInterrupt(e); + } + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java index 46452d3fc0..b8a0f6aa91 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/ConnectionOptionsTest.java @@ -17,10 +17,13 @@ package com.google.cloud.spanner.connection; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; +import com.google.cloud.NoCredentials; import com.google.cloud.spanner.SpannerOptions; import java.util.Arrays; import org.junit.Test; @@ -118,6 +121,34 @@ public void testBuildWithLocalhostPortAndValidURI() { assertThat(options.isReadOnly()).isEqualTo(ConnectionOptions.DEFAULT_READONLY); } + @Test + public void testBuildWithAutoConfigEmulator() { + ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); + builder.setUri( + "cloudspanner:/projects/test-project-123/instances/test-instance-123/databases/test-database-123?autoConfigEmulator=true"); + ConnectionOptions options = builder.build(); + assertEquals("http://localhost:9010", options.getHost()); + assertEquals("test-project-123", options.getProjectId()); + assertEquals("test-instance-123", options.getInstanceId()); + assertEquals("test-database-123", options.getDatabaseName()); + assertEquals(NoCredentials.getInstance(), options.getCredentials()); + assertTrue(options.isUsePlainText()); + } + + @Test + public void testBuildWithAutoConfigEmulatorAndHost() { + ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); + builder.setUri( + "cloudspanner://central-emulator.local:8080/projects/test-project-123/instances/test-instance-123/databases/test-database-123?autoConfigEmulator=true"); + ConnectionOptions options = builder.build(); + assertEquals("http://central-emulator.local:8080", options.getHost()); + assertEquals("test-project-123", options.getProjectId()); + assertEquals("test-instance-123", options.getInstanceId()); + assertEquals("test-database-123", options.getDatabaseName()); + assertEquals(NoCredentials.getInstance(), options.getCredentials()); + assertTrue(options.isUsePlainText()); + } + @Test public void testBuildWithDefaultProjectPlaceholder() { ConnectionOptions.Builder builder = ConnectionOptions.newBuilder(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java new file mode 100644 index 0000000000..94aea2adee --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/EmulatorUtilTest.java @@ -0,0 +1,294 @@ +/* + * 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.spanner.connection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.gax.longrunning.OperationFuture; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.Database; +import com.google.cloud.spanner.DatabaseAdminClient; +import com.google.cloud.spanner.DatabaseId; +import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.Instance; +import com.google.cloud.spanner.InstanceAdminClient; +import com.google.cloud.spanner.InstanceConfigId; +import com.google.cloud.spanner.InstanceId; +import com.google.cloud.spanner.InstanceInfo; +import com.google.cloud.spanner.Spanner; +import com.google.cloud.spanner.SpannerException; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.cloud.spanner.SpannerOptions; +import com.google.common.collect.ImmutableList; +import com.google.spanner.admin.database.v1.CreateDatabaseMetadata; +import com.google.spanner.admin.instance.v1.CreateInstanceMetadata; +import java.util.concurrent.ExecutionException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Matchers; + +@RunWith(JUnit4.class) +public class EmulatorUtilTest { + + @Test + public void testCreateInstanceAndDatabase_bothSucceed() + throws InterruptedException, ExecutionException { + Spanner spanner = mock(Spanner.class); + SpannerOptions options = mock(SpannerOptions.class); + when(spanner.getOptions()).thenReturn(options); + when(options.getCredentials()).thenReturn(NoCredentials.getInstance()); + + InstanceAdminClient instanceClient = mock(InstanceAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture instanceOperationFuture = + mock(OperationFuture.class); + + when(spanner.getInstanceAdminClient()).thenReturn(instanceClient); + when(instanceClient.createInstance(any(InstanceInfo.class))) + .thenReturn(instanceOperationFuture); + when(instanceOperationFuture.get()).thenReturn(mock(Instance.class)); + + DatabaseAdminClient databaseClient = mock(DatabaseAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture databaseOperationFuture = + mock(OperationFuture.class); + + when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient); + when(databaseClient.createDatabase( + Matchers.eq("test-instance"), + Matchers.eq("test-database"), + Matchers.eq(ImmutableList.of()))) + .thenReturn(databaseOperationFuture); + when(databaseOperationFuture.get()).thenReturn(mock(Database.class)); + + EmulatorUtil.maybeCreateInstanceAndDatabase( + spanner, DatabaseId.of("test-project", "test-instance", "test-database")); + + // Verify that both the instance and the database was created. + verify(instanceClient) + .createInstance( + InstanceInfo.newBuilder(InstanceId.of("test-project", "test-instance")) + .setDisplayName("Automatically Generated Test Instance") + .setInstanceConfigId(InstanceConfigId.of("test-project", "emulator-config")) + .setNodeCount(1) + .build()); + verify(databaseClient) + .createDatabase("test-instance", "test-database", ImmutableList.of()); + } + + @Test + public void testCreateInstanceAndDatabase_bothFailWithAlreadyExists() + throws InterruptedException, ExecutionException { + Spanner spanner = mock(Spanner.class); + SpannerOptions options = mock(SpannerOptions.class); + when(spanner.getOptions()).thenReturn(options); + when(options.getCredentials()).thenReturn(NoCredentials.getInstance()); + + InstanceAdminClient instanceClient = mock(InstanceAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture instanceOperationFuture = + mock(OperationFuture.class); + + when(spanner.getInstanceAdminClient()).thenReturn(instanceClient); + when(instanceClient.createInstance(any(InstanceInfo.class))) + .thenReturn(instanceOperationFuture); + when(instanceOperationFuture.get()) + .thenThrow( + new ExecutionException( + SpannerExceptionFactory.newSpannerException( + ErrorCode.ALREADY_EXISTS, "Instance already exists"))); + + DatabaseAdminClient databaseClient = mock(DatabaseAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture databaseOperationFuture = + mock(OperationFuture.class); + + when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient); + when(databaseClient.createDatabase( + Matchers.eq("test-instance"), + Matchers.eq("test-database"), + Matchers.eq(ImmutableList.of()))) + .thenReturn(databaseOperationFuture); + when(databaseOperationFuture.get()) + .thenThrow( + new ExecutionException( + SpannerExceptionFactory.newSpannerException( + ErrorCode.ALREADY_EXISTS, "Database already exists"))); + + EmulatorUtil.maybeCreateInstanceAndDatabase( + spanner, DatabaseId.of("test-project", "test-instance", "test-database")); + + // Verify that both the instance and the database was created. + verify(instanceClient) + .createInstance( + InstanceInfo.newBuilder(InstanceId.of("test-project", "test-instance")) + .setDisplayName("Automatically Generated Test Instance") + .setInstanceConfigId(InstanceConfigId.of("test-project", "emulator-config")) + .setNodeCount(1) + .build()); + verify(databaseClient) + .createDatabase("test-instance", "test-database", ImmutableList.of()); + } + + @Test + public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnInstanceCreation() + throws InterruptedException, ExecutionException { + Spanner spanner = mock(Spanner.class); + SpannerOptions options = mock(SpannerOptions.class); + when(spanner.getOptions()).thenReturn(options); + when(options.getCredentials()).thenReturn(NoCredentials.getInstance()); + + InstanceAdminClient instanceClient = mock(InstanceAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture instanceOperationFuture = + mock(OperationFuture.class); + + when(spanner.getInstanceAdminClient()).thenReturn(instanceClient); + when(instanceClient.createInstance(any(InstanceInfo.class))) + .thenReturn(instanceOperationFuture); + when(instanceOperationFuture.get()) + .thenThrow( + new ExecutionException( + SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "Invalid instance options"))); + + try { + EmulatorUtil.maybeCreateInstanceAndDatabase( + spanner, DatabaseId.of("test-project", "test-instance", "test-database")); + fail("missing expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + } + } + + @Test + public void testCreateInstanceAndDatabase_propagatesInterruptsOnInstanceCreation() + throws InterruptedException, ExecutionException { + Spanner spanner = mock(Spanner.class); + SpannerOptions options = mock(SpannerOptions.class); + when(spanner.getOptions()).thenReturn(options); + when(options.getCredentials()).thenReturn(NoCredentials.getInstance()); + + InstanceAdminClient instanceClient = mock(InstanceAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture instanceOperationFuture = + mock(OperationFuture.class); + + when(spanner.getInstanceAdminClient()).thenReturn(instanceClient); + when(instanceClient.createInstance(any(InstanceInfo.class))) + .thenReturn(instanceOperationFuture); + when(instanceOperationFuture.get()).thenThrow(new InterruptedException()); + + try { + EmulatorUtil.maybeCreateInstanceAndDatabase( + spanner, DatabaseId.of("test-project", "test-instance", "test-database")); + fail("missing expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.CANCELLED, e.getErrorCode()); + } + } + + @Test + public void testCreateInstanceAndDatabase_propagatesOtherErrorsOnDatabaseCreation() + throws InterruptedException, ExecutionException { + Spanner spanner = mock(Spanner.class); + SpannerOptions options = mock(SpannerOptions.class); + when(spanner.getOptions()).thenReturn(options); + when(options.getCredentials()).thenReturn(NoCredentials.getInstance()); + + InstanceAdminClient instanceClient = mock(InstanceAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture instanceOperationFuture = + mock(OperationFuture.class); + + when(spanner.getInstanceAdminClient()).thenReturn(instanceClient); + when(instanceClient.createInstance(any(InstanceInfo.class))) + .thenReturn(instanceOperationFuture); + when(instanceOperationFuture.get()).thenReturn(mock(Instance.class)); + + DatabaseAdminClient databaseClient = mock(DatabaseAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture databaseOperationFuture = + mock(OperationFuture.class); + + when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient); + when(databaseClient.createDatabase( + Matchers.eq("test-instance"), + Matchers.eq("test-database"), + Matchers.eq(ImmutableList.of()))) + .thenReturn(databaseOperationFuture); + when(databaseOperationFuture.get()) + .thenThrow( + new ExecutionException( + SpannerExceptionFactory.newSpannerException( + ErrorCode.INVALID_ARGUMENT, "Invalid database options"))); + + try { + EmulatorUtil.maybeCreateInstanceAndDatabase( + spanner, DatabaseId.of("test-project", "test-instance", "test-database")); + fail("missing expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.INVALID_ARGUMENT, e.getErrorCode()); + } + } + + @Test + public void testCreateInstanceAndDatabase_propagatesInterruptsOnDatabaseCreation() + throws InterruptedException, ExecutionException { + Spanner spanner = mock(Spanner.class); + SpannerOptions options = mock(SpannerOptions.class); + when(spanner.getOptions()).thenReturn(options); + when(options.getCredentials()).thenReturn(NoCredentials.getInstance()); + + InstanceAdminClient instanceClient = mock(InstanceAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture instanceOperationFuture = + mock(OperationFuture.class); + + when(spanner.getInstanceAdminClient()).thenReturn(instanceClient); + when(instanceClient.createInstance(any(InstanceInfo.class))) + .thenReturn(instanceOperationFuture); + when(instanceOperationFuture.get()).thenReturn(mock(Instance.class)); + + DatabaseAdminClient databaseClient = mock(DatabaseAdminClient.class); + @SuppressWarnings("unchecked") + OperationFuture databaseOperationFuture = + mock(OperationFuture.class); + + when(spanner.getDatabaseAdminClient()).thenReturn(databaseClient); + when(databaseClient.createDatabase( + Matchers.eq("test-instance"), + Matchers.eq("test-database"), + Matchers.eq(ImmutableList.of()))) + .thenReturn(databaseOperationFuture); + when(databaseOperationFuture.get()).thenThrow(new InterruptedException()); + + try { + EmulatorUtil.maybeCreateInstanceAndDatabase( + spanner, DatabaseId.of("test-project", "test-instance", "test-database")); + fail("missing expected exception"); + } catch (SpannerException e) { + assertEquals(ErrorCode.CANCELLED, e.getErrorCode()); + } + } +}