|
| 1 | +// Copyright 2025 Google LLC |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +package main |
| 16 | + |
| 17 | +import ( |
| 18 | + "bytes" |
| 19 | + "context" |
| 20 | + "fmt" |
| 21 | + "io" |
| 22 | + "os" |
| 23 | + "testing" |
| 24 | + |
| 25 | + database "cloud.google.com/go/spanner/admin/database/apiv1" |
| 26 | + adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" |
| 27 | + instance "cloud.google.com/go/spanner/admin/instance/apiv1" |
| 28 | + "cloud.google.com/go/spanner/admin/instance/apiv1/instancepb" |
| 29 | + "github.com/docker/docker/api/types/container" |
| 30 | + "github.com/googleapis/go-sql-spanner/examples/samples" |
| 31 | + "github.com/testcontainers/testcontainers-go" |
| 32 | + "github.com/testcontainers/testcontainers-go/wait" |
| 33 | +) |
| 34 | + |
| 35 | +func TestSamples(t *testing.T) { |
| 36 | + projectID := "emulator-project" |
| 37 | + instanceID := "test-instance" |
| 38 | + databaseID := "test-database" |
| 39 | + databaseName := fmt.Sprintf("projects/%s/instances/%s/databases/%s", projectID, instanceID, databaseID) |
| 40 | + |
| 41 | + emulator, err := startEmulator(projectID, instanceID, databaseID) |
| 42 | + if err != nil { |
| 43 | + if emulator != nil { |
| 44 | + emulator.Terminate(context.Background()) |
| 45 | + } |
| 46 | + t.Fatalf("failed to start emulator: %v", err) |
| 47 | + |
| 48 | + } |
| 49 | + defer emulator.Terminate(context.Background()) |
| 50 | + |
| 51 | + ctx := context.Background() |
| 52 | + var b bytes.Buffer |
| 53 | + |
| 54 | + testSample(t, ctx, &b, databaseName, samples.CreateTables, "CreateTables", fmt.Sprintf("Created Singers & Albums tables in database: [%s]\n", databaseName)) |
| 55 | + testSample(t, ctx, &b, databaseName, samples.CreateConnection, "CreateConnection", "Greeting from Spanner: Hello world!\n") |
| 56 | + testSample(t, ctx, &b, databaseName, samples.WriteDataWithDml, "WriteDataWithDml", "4 records inserted\n") |
| 57 | + testSample(t, ctx, &b, databaseName, samples.WriteDataWithDmlBatch, "WriteDataWithDmlBatch", "3 records inserted\n") |
| 58 | + testSample(t, ctx, &b, databaseName, samples.WriteDataWithMutations, "WriteDataWithMutations", "Inserted 10 rows\n") |
| 59 | + testSample(t, ctx, &b, databaseName, samples.QueryData, "QueryData", "1 1 Total Junk\n1 2 Go, Go, Go\n2 1 Green\n2 2 Forever Hold Your Peace\n2 3 Terrified\n") |
| 60 | + testSample(t, ctx, &b, databaseName, samples.QueryDataWithParameter, "QueryDataWithParameter", "12 Melissa Garcia\n") |
| 61 | + testSample(t, ctx, &b, databaseName, samples.QueryDataWithTimeout, "QueryDataWithTimeout", "") |
| 62 | + testSample(t, ctx, &b, databaseName, samples.AddColumn, "AddColumn", "Added MarketingBudget column\n") |
| 63 | + testSample(t, ctx, &b, databaseName, samples.DdlBatch, "DdlBatch", "Added Venues and Concerts tables\n") |
| 64 | + testSample(t, ctx, &b, databaseName, samples.UpdateDataWithMutations, "UpdateDataWithMutations", "Updated 2 albums\n") |
| 65 | + testSample(t, ctx, &b, databaseName, samples.QueryNewColumn, "QueryNewColumn", "1 1 100000\n1 2 NULL\n2 1 NULL\n2 2 500000\n2 3 NULL\n") |
| 66 | + testSample(t, ctx, &b, databaseName, samples.WriteWithTransactionUsingDml, "WriteWithTransactionUsingDml", "Transferred marketing budget from Album 2 to Album 1\n") |
| 67 | + testSample(t, ctx, &b, databaseName, samples.Tags, "Tags", "Reduced marketing budget\n") |
| 68 | + testSample(t, ctx, &b, databaseName, samples.ReadOnlyTransaction, "ReadOnlyTransaction", "1 1 Total Junk\n1 2 Go, Go, Go\n2 1 Green\n2 2 Forever Hold Your Peace\n2 3 Terrified\n2 2 Forever Hold Your Peace\n1 2 Go, Go, Go\n2 1 Green\n2 3 Terrified\n1 1 Total Junk\n") |
| 69 | + testSample(t, ctx, &b, databaseName, samples.DataBoost, "DataBoost", "1 Marc Richards\n2 Catalina Smith\n3 Alice Trentor\n4 Lea Martin\n5 David Lomond\n12 Melissa Garcia\n13 Russel Morales\n14 Jacqueline Long\n15 Dylan Shaw\n16 Sarah Wilson\n17 Ethan Miller\n18 Maya Patel\n") |
| 70 | + testSample(t, ctx, &b, databaseName, samples.PartitionedDml, "PDML", "Updated at least 3 albums\n") |
| 71 | +} |
| 72 | + |
| 73 | +func testSample(t *testing.T, ctx context.Context, b *bytes.Buffer, databaseName string, sample func(ctx context.Context, w io.Writer, databaseName string) error, sampleName, want string) { |
| 74 | + if err := sample(ctx, b, databaseName); err != nil { |
| 75 | + t.Fatalf("failed to run %s: %v", sampleName, err) |
| 76 | + } |
| 77 | + if g, w := b.String(), want; g != w { |
| 78 | + t.Fatalf("%s output mismatch\n Got: %v\nWant: %v", sampleName, g, w) |
| 79 | + } |
| 80 | + b.Reset() |
| 81 | +} |
| 82 | + |
| 83 | +func startEmulator(projectID, instanceID, databaseID string) (testcontainers.Container, error) { |
| 84 | + ctx := context.Background() |
| 85 | + req := testcontainers.ContainerRequest{ |
| 86 | + AlwaysPullImage: true, |
| 87 | + Image: "gcr.io/cloud-spanner-emulator/emulator", |
| 88 | + ExposedPorts: []string{"9010/tcp"}, |
| 89 | + WaitingFor: wait.ForListeningPort("9010/tcp"), |
| 90 | + HostConfigModifier: func(hostConfig *container.HostConfig) { |
| 91 | + hostConfig.AutoRemove = true |
| 92 | + }, |
| 93 | + } |
| 94 | + emulator, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ |
| 95 | + ContainerRequest: req, |
| 96 | + Started: true, |
| 97 | + }) |
| 98 | + if err != nil { |
| 99 | + return emulator, fmt.Errorf("failed to start PGAdapter: %v", err) |
| 100 | + } |
| 101 | + host, err := emulator.Host(ctx) |
| 102 | + if err != nil { |
| 103 | + return emulator, fmt.Errorf("failed to get host: %v", err) |
| 104 | + } |
| 105 | + mappedPort, err := emulator.MappedPort(ctx, "9010/tcp") |
| 106 | + if err != nil { |
| 107 | + return emulator, fmt.Errorf("failed to get mapped port: %v", err) |
| 108 | + } |
| 109 | + port := mappedPort.Int() |
| 110 | + // Set the env var to connec to the emulator. |
| 111 | + if err := os.Setenv("SPANNER_EMULATOR_HOST", fmt.Sprintf("%s:%v", host, port)); err != nil { |
| 112 | + return emulator, fmt.Errorf("failed to set env var for emulator: %v", err) |
| 113 | + } |
| 114 | + if err := createInstance(projectID, instanceID); err != nil { |
| 115 | + return emulator, fmt.Errorf("failed to create instance: %v", err) |
| 116 | + } |
| 117 | + if err := createDatabase(projectID, instanceID, databaseID); err != nil { |
| 118 | + return emulator, fmt.Errorf("failed to create database: %v", err) |
| 119 | + } |
| 120 | + return emulator, nil |
| 121 | +} |
| 122 | + |
| 123 | +func createInstance(projectID, instanceID string) error { |
| 124 | + ctx := context.Background() |
| 125 | + instanceAdmin, err := instance.NewInstanceAdminClient(ctx) |
| 126 | + if err != nil { |
| 127 | + return err |
| 128 | + } |
| 129 | + defer instanceAdmin.Close() |
| 130 | + |
| 131 | + op, err := instanceAdmin.CreateInstance(ctx, &instancepb.CreateInstanceRequest{ |
| 132 | + Parent: fmt.Sprintf("projects/%s", projectID), |
| 133 | + InstanceId: instanceID, |
| 134 | + Instance: &instancepb.Instance{ |
| 135 | + Config: fmt.Sprintf("projects/%s/instanceConfigs/%s", projectID, "regional-us-central1"), |
| 136 | + DisplayName: instanceID, |
| 137 | + NodeCount: 1, |
| 138 | + Labels: map[string]string{"cloud_spanner_samples": "true"}, |
| 139 | + Edition: instancepb.Instance_STANDARD, |
| 140 | + }, |
| 141 | + }) |
| 142 | + if err != nil { |
| 143 | + return fmt.Errorf("could not create instance %s: %w", fmt.Sprintf("projects/%s/instances/%s", projectID, instanceID), err) |
| 144 | + } |
| 145 | + // Wait for the instance creation to finish. |
| 146 | + if _, err = op.Wait(ctx); err != nil { |
| 147 | + return fmt.Errorf("waiting for instance creation to finish failed: %w", err) |
| 148 | + } |
| 149 | + return nil |
| 150 | +} |
| 151 | + |
| 152 | +func createDatabase(projectID, instanceID, databaseID string) error { |
| 153 | + ctx := context.Background() |
| 154 | + adminClient, err := database.NewDatabaseAdminClient(ctx) |
| 155 | + if err != nil { |
| 156 | + return err |
| 157 | + } |
| 158 | + defer adminClient.Close() |
| 159 | + |
| 160 | + op, err := adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{ |
| 161 | + Parent: fmt.Sprintf("projects/%s/instances/%s", projectID, instanceID), |
| 162 | + CreateStatement: fmt.Sprintf("CREATE DATABASE `%s`", databaseID), |
| 163 | + }) |
| 164 | + if err != nil { |
| 165 | + return err |
| 166 | + } |
| 167 | + if _, err := op.Wait(ctx); err != nil { |
| 168 | + return err |
| 169 | + } |
| 170 | + return nil |
| 171 | +} |
0 commit comments