aboutsummaryrefslogtreecommitdiffstats
path: root/dashboard
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2025-12-29 12:43:14 +0100
committerDmitry Vyukov <dvyukov@google.com>2025-12-31 16:40:48 +0000
commit2733a37369ec000956672447ea0eb130815102bd (patch)
tree7ffccff813d1216a83a9ae2dfb547878b405a426 /dashboard
parent51c522f1591fe44ec87d216caf93ae0aa8179a7c (diff)
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.
Diffstat (limited to 'dashboard')
-rw-r--r--dashboard/app/ai_test.go26
-rw-r--r--dashboard/app/aidb/crud.go9
-rw-r--r--dashboard/app/aidb/migrations/1_initialize.down.sql1
-rw-r--r--dashboard/app/aidb/migrations/1_initialize.up.sql3
-rw-r--r--dashboard/app/util_test.go57
5 files changed, 96 insertions, 0 deletions
diff --git a/dashboard/app/ai_test.go b/dashboard/app/ai_test.go
new file mode 100644
index 000000000..51f6142fd
--- /dev/null
+++ b/dashboard/app/ai_test.go
@@ -0,0 +1,26 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package main
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAIMigrations(t *testing.T) {
+ // Ensure spanner DDL files are syntax-correct and idempotent.
+ // NewSpannerCtx already run the "up" statements, so we start with "down".
+ c := NewSpannerCtx(t)
+ defer c.Close()
+
+ up, err := loadDDLStatements("1_initialize.up.sql")
+ require.NoError(t, err)
+ down, err := loadDDLStatements("1_initialize.down.sql")
+ require.NoError(t, err)
+
+ require.NoError(t, executeSpannerDDL(c.ctx, down))
+ require.NoError(t, executeSpannerDDL(c.ctx, up))
+ require.NoError(t, executeSpannerDDL(c.ctx, down))
+}
diff --git a/dashboard/app/aidb/crud.go b/dashboard/app/aidb/crud.go
new file mode 100644
index 000000000..85614b05c
--- /dev/null
+++ b/dashboard/app/aidb/crud.go
@@ -0,0 +1,9 @@
+// Copyright 2025 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package aidb
+
+const (
+ Instance = "syzbot"
+ Database = "ai"
+)
diff --git a/dashboard/app/aidb/migrations/1_initialize.down.sql b/dashboard/app/aidb/migrations/1_initialize.down.sql
new file mode 100644
index 000000000..d65b9e742
--- /dev/null
+++ b/dashboard/app/aidb/migrations/1_initialize.down.sql
@@ -0,0 +1 @@
+DROP TABLE Test;
diff --git a/dashboard/app/aidb/migrations/1_initialize.up.sql b/dashboard/app/aidb/migrations/1_initialize.up.sql
new file mode 100644
index 000000000..981cd8d3a
--- /dev/null
+++ b/dashboard/app/aidb/migrations/1_initialize.up.sql
@@ -0,0 +1,3 @@
+CREATE TABLE Test (
+ Name STRING(1000) NOT NULL,
+) PRIMARY KEY (Name);
diff --git a/dashboard/app/util_test.go b/dashboard/app/util_test.go
index 9fd4d3b81..eb18a38a9 100644
--- a/dashboard/app/util_test.go
+++ b/dashboard/app/util_test.go
@@ -22,16 +22,22 @@ import (
"runtime"
"strings"
"sync"
+ "sync/atomic"
"testing"
"time"
+ "cloud.google.com/go/spanner/admin/database/apiv1"
+ "cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
"github.com/google/go-cmp/cmp"
"github.com/google/syzkaller/dashboard/api"
+ "github.com/google/syzkaller/dashboard/app/aidb"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/coveragedb/spannerclient"
"github.com/google/syzkaller/pkg/covermerger"
"github.com/google/syzkaller/pkg/email"
"github.com/google/syzkaller/pkg/subsystem"
+ spannertest "github.com/google/syzkaller/syz-cluster/pkg/db"
+ "google.golang.org/appengine/v2"
"google.golang.org/appengine/v2/aetest"
db "google.golang.org/appengine/v2/datastore"
"google.golang.org/appengine/v2/log"
@@ -58,11 +64,16 @@ var skipDevAppserverTests = func() bool {
}()
func NewCtx(t *testing.T) *Ctx {
+ return newCtx(t, "")
+}
+
+func newCtx(t *testing.T, appID string) *Ctx {
if skipDevAppserverTests {
t.Skip("skipping test (no dev_appserver.py)")
}
t.Parallel()
inst, err := aetest.NewInstance(&aetest.Options{
+ AppID: appID,
StartupTimeout: 120 * time.Second,
// Without this option datastore queries return data with slight delay,
// which fails reporting tests.
@@ -89,6 +100,52 @@ func NewCtx(t *testing.T) *Ctx {
return c
}
+var appIDSeq = uint32(0)
+
+func NewSpannerCtx(t *testing.T) *Ctx {
+ ddlStatements, err := loadDDLStatements("1_initialize.up.sql")
+ if err != nil {
+ t.Fatal(err)
+ }
+ // The code uses AppID as the spanner database URI project.
+ // So to give each test a private isolated instance of the spanner database,
+ // we give each test that uses spanner an unique AppID.
+ appID := fmt.Sprintf("testapp-%v", atomic.AddUint32(&appIDSeq, 1))
+ uri := fmt.Sprintf("projects/%s/instances/%v/databases/%v", appID, aidb.Instance, aidb.Database)
+ spannertest.NewTestDB(t, uri, ddlStatements)
+ return newCtx(t, appID)
+}
+
+func executeSpannerDDL(ctx context.Context, statements []string) error {
+ dbAdmin, err := database.NewDatabaseAdminClient(ctx)
+ if err != nil {
+ return fmt.Errorf("failed NewDatabaseAdminClient: %w", err)
+ }
+ defer dbAdmin.Close()
+ dbOp, err := dbAdmin.UpdateDatabaseDdl(ctx, &databasepb.UpdateDatabaseDdlRequest{
+ Database: fmt.Sprintf("projects/%s/instances/%v/databases/%v",
+ appengine.AppID(ctx), aidb.Instance, aidb.Database),
+ Statements: statements,
+ })
+ if err != nil {
+ return fmt.Errorf("failed UpdateDatabaseDdl: %w", err)
+ }
+ if err := dbOp.Wait(ctx); err != nil {
+ return fmt.Errorf("failed UpdateDatabaseDdl: %w", err)
+ }
+ return nil
+}
+
+func loadDDLStatements(file string) ([]string, error) {
+ data, err := os.ReadFile(filepath.Join("aidb", "migrations", file))
+ if err != nil {
+ return nil, err
+ }
+ // We need individual statements. Assume semicolon is not used in other places than statements end.
+ statements := strings.Split(string(data), ";")
+ return statements[:len(statements)-1], nil
+}
+
func (c *Ctx) config() *GlobalConfig {
return getConfig(c.ctx)
}