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());
+ }
+ }
+}