Skip to content

Commit

Permalink
Add Go tests in MySQL storage implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
roger2hk committed Aug 1, 2024
1 parent bc6fcfe commit 0228778
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 1 deletion.
14 changes: 13 additions & 1 deletion storage/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"bytes"
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
Expand Down Expand Up @@ -70,6 +71,9 @@ func New(ctx context.Context, db *sql.DB, opts ...func(*tessera.StorageOptions))
klog.Errorf("Failed to ping database: %v", err)
return nil, err
}
if s.newCheckpoint == nil || s.parseCheckpoint == nil {
return nil, errors.New("tessera.WithCheckpointSignerVerifier must be provided in New()")
}

s.queue = storage.NewQueue(ctx, opt.BatchMaxAge, opt.BatchMaxSize, s.sequenceBatch)

Expand Down Expand Up @@ -187,7 +191,15 @@ func (s *Storage) ReadEntryBundle(ctx context.Context, index uint64) ([]byte, er
}

var entryBundle []byte
return entryBundle, row.Scan(&entryBundle)
if err := row.Scan(&entryBundle); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}

return nil, err
}

return entryBundle, nil
}

func (s *Storage) writeEntryBundle(ctx context.Context, tx *sql.Tx, index uint64, entryBundle []byte) error {
Expand Down
274 changes: 274 additions & 0 deletions storage/mysql/mysql_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
// Copyright 2024 The Tessera authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package mysql_test contains the tests for a MySQL-based storage implementation for Tessera.
// It requires a MySQL database to successfully run the tests. Otherwise, the tests in this file will be skipped.
//
// Sample command to start a local MySQL database using Docker:
// $ docker run --name test-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test_tessera -d mysql
package mysql_test

import (
Expand All @@ -6,15 +25,31 @@ import (
"flag"
"os"
"testing"
"time"

tessera "github.com/transparency-dev/trillian-tessera"
"github.com/transparency-dev/trillian-tessera/storage/mysql"
"golang.org/x/mod/sumdb/note"
"k8s.io/klog/v2"
)

var (
mysqlURI = flag.String("mysql_uri", "root:root@tcp(localhost:3306)/test_tessera", "Connection string for a MySQL database")
isMySQLTestOptional = flag.Bool("is_mysql_test_optional", true, "Boolean value to control whether the MySQL test is optional")

testDB *sql.DB
noteSigner note.Signer
noteVerifier note.Verifier
)

const (
testPrivateKey = "PRIVATE+KEY+Test-Betty+df84580a+Afge8kCzBXU7jb3cV2Q363oNXCufJ6u9mjOY1BGRY9E2"
testPublicKey = "Test-Betty+df84580a+AQQASqPUZoIHcJAF5mBOryctwFdTV1E0GRY4kEAtTzwB"
)

// TestMain checks whether the test MySQL database is available and starts the tests including database schema initialization.
// If is_mysql_test_optional is set to true and MySQL database cannot be opened or pinged, the test will fail immediately.
// Otherwise, the test will be skipped if the test is optional and the database is not available.
func TestMain(m *testing.M) {
klog.InitFlags(nil)
flag.Parse()
Expand All @@ -28,14 +63,253 @@ func TestMain(m *testing.M) {
}
klog.Fatalf("Failed to open MySQL test db: %v", err)
}
defer func() {
if err := db.Close(); err != nil {
klog.Warningf("Failed to close MySQL database: %v", err)
}
}()
if err := db.PingContext(ctx); err != nil {
if *isMySQLTestOptional {
klog.Warning("MySQL not available, skipping all MySQL storage tests")
return
}
klog.Fatalf("Failed to ping MySQL test db: %v", err)
}
testDB = db

klog.Info("Successfully connected to MySQL test database")

initDatabaseSchema(ctx)

noteSigner, err = note.NewSigner(testPrivateKey)
if err != nil {
klog.Fatalf("Failed to create new signer: %v", err)
}
noteVerifier, err = note.NewVerifier(testPublicKey)
if err != nil {
klog.Fatalf("Failed to create new verifier: %v", err)
}

os.Exit(m.Run())
}

// initDatabaseSchema drops the tables and then imports the schema.
func initDatabaseSchema(ctx context.Context) {
dropTablesSQL := "DROP TABLE IF EXISTS `Checkpoint`, `Subtree`, `TiledLeaves`"

rawSchema, err := os.ReadFile("schema.sql")
if err != nil {
klog.Fatalf("Failed to read schema.sql: %v", err)
}

db, err := sql.Open("mysql", *mysqlURI+"?multiStatements=true")
if err != nil {
klog.Fatalf("Failed to connect to DB: %v", err)
}
defer func() {
if err := db.Close(); err != nil {
klog.Warningf("Failed to close db: %v", err)
}
}()

if _, err := db.ExecContext(ctx, dropTablesSQL); err != nil {
klog.Fatalf("Failed to drop all tables: %v", err)
}

if _, err := db.ExecContext(ctx, string(rawSchema)); err != nil {
klog.Fatalf("Failed to execute init database schema: %v", err)
}
}

func TestNew(t *testing.T) {
ctx := context.Background()

for _, test := range []struct {
name string
opts []func(*tessera.StorageOptions)
wantErr bool
}{
{
name: "no tessera.StorageOption",
opts: nil,
wantErr: true,
},
{
name: "standard tessera.WithCheckpointSignerVerifier",
opts: []func(*tessera.StorageOptions){
tessera.WithCheckpointSignerVerifier(noteSigner, noteVerifier),
},
wantErr: false,
},
{
name: "all tessera.StorageOption",
opts: []func(*tessera.StorageOptions){
tessera.WithCheckpointSignerVerifier(noteSigner, noteVerifier),
tessera.WithBatching(1, 1*time.Second),
tessera.WithPushback(10),
},
wantErr: false,
},
} {
t.Run(test.name, func(t *testing.T) {
_, err := mysql.New(ctx, testDB, test.opts...)
gotErr := err != nil
if gotErr != test.wantErr {
t.Errorf("got err %v want %v", gotErr, test.wantErr)
}
})
}
}

func TestReadTile(t *testing.T) {
ctx := context.Background()
s := newTestMySQLStorage(t, ctx)

for _, test := range []struct {
name string
level, index, width uint64
wantErr bool
}{
{
name: "0/0/0",
level: 0, index: 0, width: 0,
wantErr: false,
},
{
name: "123/456/789",
level: 123, index: 456, width: 789,
wantErr: false,
},
} {
t.Run(test.name, func(t *testing.T) {
_, err := s.ReadTile(ctx, test.level, test.index, test.width)
gotErr := err != nil
if gotErr != test.wantErr {
t.Errorf("got err %v want %v", gotErr, test.wantErr)
}
})
}
}

func TestReadEntryBundle(t *testing.T) {
ctx := context.Background()
s := newTestMySQLStorage(t, ctx)

for _, test := range []struct {
name string
index uint64
wantErr bool
}{
{
name: "0",
index: 0,
wantErr: false,
},
{
name: "123456789",
index: 123456789,
wantErr: false,
},
} {
t.Run(test.name, func(t *testing.T) {
_, err := s.ReadEntryBundle(ctx, test.index)
gotErr := err != nil
if gotErr != test.wantErr {
t.Errorf("got err %v want %v", gotErr, test.wantErr)
}
})
}
}

func TestAdd(t *testing.T) {
ctx := context.Background()
s := newTestMySQLStorage(t, ctx)

for _, test := range []struct {
name string
entry []byte
wantIndex uint64
wantErr bool
}{
{
name: "empty string entry",
entry: []byte(""),
wantIndex: 0,
wantErr: false,
},
{
name: "123 string entry",
entry: []byte("123"),
wantIndex: 1,
wantErr: false,
},
{
name: "empty byte",
entry: []byte{},
wantIndex: 2,
wantErr: false,
},
} {
t.Run(test.name, func(t *testing.T) {
index, err := s.Add(ctx, tessera.NewEntry(test.entry))
gotErr := err != nil
if gotErr != test.wantErr {
t.Errorf("got err %v want %v", gotErr, test.wantErr)
}
if index != test.wantIndex {
t.Errorf("got index %d want %d", index, test.wantIndex)
}
})
}
}

func TestParallelAdd(t *testing.T) {
t.Parallel()
ctx := context.Background()
s := newTestMySQLStorage(t, ctx)

for _, test := range []struct {
name string
entry []byte
wantErr bool
}{
{
name: "empty string entry",
entry: []byte(""),
wantErr: false,
},
{
name: "123 string entry",
entry: []byte("123"),
wantErr: false,
},
{
name: "empty byte",
entry: []byte{},
wantErr: false,
},
} {
t.Run(test.name, func(t *testing.T) {
for i := 0; i < 655536; i++ {
go func() {
_, err := s.Add(ctx, tessera.NewEntry(test.entry))
gotErr := err != nil
if gotErr != test.wantErr {
t.Errorf("got err %v want %v", gotErr, test.wantErr)
}
}()
}
})
}
}

func newTestMySQLStorage(t *testing.T, ctx context.Context) *mysql.Storage {
t.Helper()

s, err := mysql.New(ctx, testDB, tessera.WithCheckpointSignerVerifier(noteSigner, noteVerifier), tessera.WithBatching(256, time.Microsecond))
if err != nil {
t.Errorf("Failed to create mysql.Storage: %v", err)
}

return s
}

0 comments on commit 0228778

Please sign in to comment.