From a00519ab8764e1e5f59a946ae5d7066c5d65dcd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 11 Jun 2026 17:23:12 +0200 Subject: [PATCH 1/3] fix(test): resolve flakiness in TestTransactionTimeout by using larger timeouts --- transaction_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/transaction_test.go b/transaction_test.go index 1592f6ba..d1ceb99f 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -325,9 +325,9 @@ func TestTransactionTimeout(t *testing.T) { defer teardown() ctx := context.Background() - server.TestSpanner.PutExecutionTime(testutil.MethodExecuteStreamingSql, testutil.SimulatedExecutionTime{MinimumExecutionTime: 50 * time.Millisecond}) + server.TestSpanner.PutExecutionTime(testutil.MethodExecuteStreamingSql, testutil.SimulatedExecutionTime{MinimumExecutionTime: 200 * time.Millisecond}) tx, _ := db.BeginTx(ctx, &sql.TxOptions{}) - if _, err := tx.ExecContext(ctx, "set local transaction_timeout=10ms"); err != nil { + if _, err := tx.ExecContext(ctx, "set local transaction_timeout=100ms"); err != nil { t.Fatal(err) } _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo) @@ -367,14 +367,14 @@ func TestTransactionTimeoutSecondStatement(t *testing.T) { ctx := context.Background() tx, _ := db.BeginTx(ctx, &sql.TxOptions{}) - if _, err := tx.ExecContext(ctx, "set local transaction_timeout=40ms"); err != nil { + if _, err := tx.ExecContext(ctx, "set local transaction_timeout=200ms"); err != nil { t.Fatal(err) } if _, err := tx.ExecContext(ctx, testutil.UpdateBarSetFoo); err != nil { t.Fatal(err) } - server.TestSpanner.PutExecutionTime(testutil.MethodExecuteStreamingSql, testutil.SimulatedExecutionTime{MinimumExecutionTime: 60 * time.Millisecond}) + server.TestSpanner.PutExecutionTime(testutil.MethodExecuteStreamingSql, testutil.SimulatedExecutionTime{MinimumExecutionTime: 300 * time.Millisecond}) rows, err := tx.QueryContext(ctx, testutil.SelectFooFromBar, ExecOptions{DirectExecuteQuery: true}) if rows != nil { _ = rows.Close() From 39210f2754bcab2a25ef9e15e593170598b537d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 11 Jun 2026 19:40:58 +0200 Subject: [PATCH 2/3] test: add TestArrayOfStruct to verify array of struct decoding via DecodeOptionProto --- driver_with_mockserver_test.go | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/driver_with_mockserver_test.go b/driver_with_mockserver_test.go index 6d90afa6..07249a5d 100644 --- a/driver_with_mockserver_test.go +++ b/driver_with_mockserver_test.go @@ -6021,3 +6021,95 @@ func filterBeginReadOnlyRequests(requests []interface{}) []*sppb.BeginTransactio } return res } + +func TestArrayOfStruct(t *testing.T) { + t.Parallel() + + db, server, teardown := setupTestDBConnection(t) + defer teardown() + + query := "SELECT ARRAY(SELECT AS STRUCT * FROM UNNEST(@entries)) AS entries" + + fields := []*sppb.StructType_Field{ + {Name: "ID", Type: &sppb.Type{Code: sppb.TypeCode_INT64}}, + {Name: "Name", Type: &sppb.Type{Code: sppb.TypeCode_STRING}}, + } + structType := &sppb.StructType{Fields: fields} + + metadata := &sppb.ResultSetMetadata{ + RowType: &sppb.StructType{ + Fields: []*sppb.StructType_Field{ + { + Name: "entries", + Type: &sppb.Type{ + Code: sppb.TypeCode_ARRAY, + ArrayElementType: &sppb.Type{ + Code: sppb.TypeCode_STRUCT, + StructType: structType, + }, + }, + }, + }, + }, + } + + struct1 := &structpb.ListValue{Values: []*structpb.Value{ + {Kind: &structpb.Value_StringValue{StringValue: "0"}}, + {Kind: &structpb.Value_StringValue{StringValue: "Hello"}}, + }} + struct2 := &structpb.ListValue{Values: []*structpb.Value{ + {Kind: &structpb.Value_StringValue{StringValue: "1"}}, + {Kind: &structpb.Value_StringValue{StringValue: "World"}}, + }} + arrayVal := &structpb.Value{Kind: &structpb.Value_ListValue{ListValue: &structpb.ListValue{Values: []*structpb.Value{ + {Kind: &structpb.Value_ListValue{ListValue: struct1}}, + {Kind: &structpb.Value_ListValue{ListValue: struct2}}, + }}}} + + resultSet := &sppb.ResultSet{ + Metadata: metadata, + Rows: []*structpb.ListValue{ + {Values: []*structpb.Value{arrayVal}}, + }, + } + + _ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{ + Type: testutil.StatementResultResultSet, + ResultSet: resultSet, + }) + + type Entry struct { + ID int64 + Name string + } + entries := []Entry{ + {ID: 0, Name: "Hello"}, + {ID: 1, Name: "World"}, + } + + ctx := context.Background() + rows, err := db.QueryContext(ctx, query, sql.Named("entries", entries), ExecOptions{DecodeOption: DecodeOptionProto}) + if err != nil { + t.Fatal(err) + } + defer rows.Close() + + if !rows.Next() { + t.Fatalf("expected rows.Next() to return true, got false, err: %v", rows.Err()) + } + var col spanner.GenericColumnValue + if err := rows.Scan(&col); err != nil { + t.Fatalf("Scan returned error: %v", err) + } + var allEntries []*Entry + if err := col.Decode(&allEntries); err != nil { + t.Fatalf("Decode returned error: %v", err) + } + expected := []*Entry{ + {ID: 0, Name: "Hello"}, + {ID: 1, Name: "World"}, + } + if !reflect.DeepEqual(allEntries, expected) { + t.Errorf("allEntries mismatch\n Got: %+v\nWant: %+v", allEntries, expected) + } +} From c531981886515d17516f1091916ec36fc762e73a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Thu, 11 Jun 2026 16:40:01 +0200 Subject: [PATCH 3/3] fix(examples): wait for emulator port to open before executing samples --- examples/emulator_runner.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/emulator_runner.go b/examples/emulator_runner.go index 59b5ce01..366d71b6 100644 --- a/examples/emulator_runner.go +++ b/examples/emulator_runner.go @@ -19,6 +19,7 @@ import ( "fmt" "io" "log" + "net" "os" "time" @@ -107,19 +108,29 @@ func startEmulator() error { if err := cli.ContainerStart(ctx, containerId, container.StartOptions{}); err != nil { return err } - // Wait max 10 seconds or until the emulator is running. - for c := 0; c < 20; c++ { - // Always wait at least 500 milliseconds to ensure that the emulator is actually ready, as the - // state can be reported as ready, while the emulator (or network interface) is actually not ready. - <-time.After(500 * time.Millisecond) + // Wait max 10 seconds or until the emulator is running and port 9010 is open. + var portReady bool + for c := 0; c < 40; c++ { resp, err := cli.ContainerInspect(ctx, containerId) if err != nil { return fmt.Errorf("failed to inspect container state: %v", err) } if resp.State.Running { - break + // Try to connect to localhost:9010 + conn, err := net.DialTimeout("tcp", "localhost:9010", 250*time.Millisecond) + if err == nil { + _ = conn.Close() + portReady = true + break + } } + <-time.After(250 * time.Millisecond) } + if !portReady { + return fmt.Errorf("emulator did not start listening on port 9010 in time") + } + // Give the emulator gRPC server a moment to fully initialize after the TCP port opens. + <-time.After(1500 * time.Millisecond) return nil }