diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2025-12-29 12:43:14 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2025-12-31 16:40:48 +0000 |
| commit | 2733a37369ec000956672447ea0eb130815102bd (patch) | |
| tree | 7ffccff813d1216a83a9ae2dfb547878b405a426 /dashboard | |
| parent | 51c522f1591fe44ec87d216caf93ae0aa8179a7c (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.go | 26 | ||||
| -rw-r--r-- | dashboard/app/aidb/crud.go | 9 | ||||
| -rw-r--r-- | dashboard/app/aidb/migrations/1_initialize.down.sql | 1 | ||||
| -rw-r--r-- | dashboard/app/aidb/migrations/1_initialize.up.sql | 3 | ||||
| -rw-r--r-- | dashboard/app/util_test.go | 57 |
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) } |
