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