Skip to content

Commit 2733a37

Browse files
committed
dashboard/app: make it possible to test code that uses spanner
Start spanner emulator for tests. Create isolated per-test instance+database. Test that DDL migration scripts are work.
1 parent 51c522f commit 2733a37

File tree

5 files changed

+96
-0
lines changed

5 files changed

+96
-0
lines changed

dashboard/app/ai_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2025 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package main
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestAIMigrations(t *testing.T) {
13+
// Ensure spanner DDL files are syntax-correct and idempotent.
14+
// NewSpannerCtx already run the "up" statements, so we start with "down".
15+
c := NewSpannerCtx(t)
16+
defer c.Close()
17+
18+
up, err := loadDDLStatements("1_initialize.up.sql")
19+
require.NoError(t, err)
20+
down, err := loadDDLStatements("1_initialize.down.sql")
21+
require.NoError(t, err)
22+
23+
require.NoError(t, executeSpannerDDL(c.ctx, down))
24+
require.NoError(t, executeSpannerDDL(c.ctx, up))
25+
require.NoError(t, executeSpannerDDL(c.ctx, down))
26+
}

dashboard/app/aidb/crud.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright 2025 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package aidb
5+
6+
const (
7+
Instance = "syzbot"
8+
Database = "ai"
9+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE Test;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
CREATE TABLE Test (
2+
Name STRING(1000) NOT NULL,
3+
) PRIMARY KEY (Name);

dashboard/app/util_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,22 @@ import (
2222
"runtime"
2323
"strings"
2424
"sync"
25+
"sync/atomic"
2526
"testing"
2627
"time"
2728

29+
"cloud.google.com/go/spanner/admin/database/apiv1"
30+
"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
2831
"github.com/google/go-cmp/cmp"
2932
"github.com/google/syzkaller/dashboard/api"
33+
"github.com/google/syzkaller/dashboard/app/aidb"
3034
"github.com/google/syzkaller/dashboard/dashapi"
3135
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
3236
"github.com/google/syzkaller/pkg/covermerger"
3337
"github.com/google/syzkaller/pkg/email"
3438
"github.com/google/syzkaller/pkg/subsystem"
39+
spannertest "github.com/google/syzkaller/syz-cluster/pkg/db"
40+
"google.golang.org/appengine/v2"
3541
"google.golang.org/appengine/v2/aetest"
3642
db "google.golang.org/appengine/v2/datastore"
3743
"google.golang.org/appengine/v2/log"
@@ -58,11 +64,16 @@ var skipDevAppserverTests = func() bool {
5864
}()
5965

6066
func NewCtx(t *testing.T) *Ctx {
67+
return newCtx(t, "")
68+
}
69+
70+
func newCtx(t *testing.T, appID string) *Ctx {
6171
if skipDevAppserverTests {
6272
t.Skip("skipping test (no dev_appserver.py)")
6373
}
6474
t.Parallel()
6575
inst, err := aetest.NewInstance(&aetest.Options{
76+
AppID: appID,
6677
StartupTimeout: 120 * time.Second,
6778
// Without this option datastore queries return data with slight delay,
6879
// which fails reporting tests.
@@ -89,6 +100,52 @@ func NewCtx(t *testing.T) *Ctx {
89100
return c
90101
}
91102

103+
var appIDSeq = uint32(0)
104+
105+
func NewSpannerCtx(t *testing.T) *Ctx {
106+
ddlStatements, err := loadDDLStatements("1_initialize.up.sql")
107+
if err != nil {
108+
t.Fatal(err)
109+
}
110+
// The code uses AppID as the spanner database URI project.
111+
// So to give each test a private isolated instance of the spanner database,
112+
// we give each test that uses spanner an unique AppID.
113+
appID := fmt.Sprintf("testapp-%v", atomic.AddUint32(&appIDSeq, 1))
114+
uri := fmt.Sprintf("projects/%s/instances/%v/databases/%v", appID, aidb.Instance, aidb.Database)
115+
spannertest.NewTestDB(t, uri, ddlStatements)
116+
return newCtx(t, appID)
117+
}
118+
119+
func executeSpannerDDL(ctx context.Context, statements []string) error {
120+
dbAdmin, err := database.NewDatabaseAdminClient(ctx)
121+
if err != nil {
122+
return fmt.Errorf("failed NewDatabaseAdminClient: %w", err)
123+
}
124+
defer dbAdmin.Close()
125+
dbOp, err := dbAdmin.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{
126+
Database: fmt.Sprintf("projects/%s/instances/%v/databases/%v",
127+
appengine.AppID(ctx), aidb.Instance, aidb.Database),
128+
Statements: statements,
129+
})
130+
if err != nil {
131+
return fmt.Errorf("failed UpdateDatabaseDdl: %w", err)
132+
}
133+
if err := dbOp.Wait(ctx); err != nil {
134+
return fmt.Errorf("failed UpdateDatabaseDdl: %w", err)
135+
}
136+
return nil
137+
}
138+
139+
func loadDDLStatements(file string) ([]string, error) {
140+
data, err := os.ReadFile(filepath.Join("aidb", "migrations", file))
141+
if err != nil {
142+
return nil, err
143+
}
144+
// We need individual statements. Assume semicolon is not used in other places than statements end.
145+
statements := strings.Split(string(data), ";")
146+
return statements[:len(statements)-1], nil
147+
}
148+
92149
func (c *Ctx) config() *GlobalConfig {
93150
return getConfig(c.ctx)
94151
}

0 commit comments

Comments
 (0)