This is an automated email from the ASF dual-hosted git repository.
jimin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-seata-go.git
The following commit(s) were added to refs/heads/master by this push:
new d76ac395 test: Improve test coverage for
pkg/datasource/sql/datasource/mysql (#965)
d76ac395 is described below
commit d76ac395326cf34fe34b7b89ddb6e0759e1e2783
Author: flypiggy <[email protected]>
AuthorDate: Mon Nov 10 00:53:53 2025 +0800
test: Improve test coverage for pkg/datasource/sql/datasource/mysql (#965)
---
pkg/datasource/sql/datasource/base/meta_cache.go | 7 +
.../sql/datasource/base/meta_cache_test.go | 84 ++++
.../sql/datasource/mysql/meta_cache_test.go | 367 ++++++++++++++
.../sql/datasource/mysql/trigger_test.go | 536 +++++++++++++++++++++
.../handler/tcc_fence_wrapper_handler_test.go | 50 +-
5 files changed, 1027 insertions(+), 17 deletions(-)
diff --git a/pkg/datasource/sql/datasource/base/meta_cache.go
b/pkg/datasource/sql/datasource/base/meta_cache.go
index 3c124ed1..80652c4a 100644
--- a/pkg/datasource/sql/datasource/base/meta_cache.go
+++ b/pkg/datasource/sql/datasource/base/meta_cache.go
@@ -88,7 +88,10 @@ func (c *BaseTableMetaCache) Init(ctx context.Context) error
{
// refresh
func (c *BaseTableMetaCache) refresh(ctx context.Context) {
f := func() {
+ // Get table names with read lock
+ c.lock.RLock()
if c.db == nil || c.cfg == nil || c.cache == nil ||
len(c.cache) == 0 {
+ c.lock.RUnlock()
return
}
@@ -96,6 +99,9 @@ func (c *BaseTableMetaCache) refresh(ctx context.Context) {
for table := range c.cache {
tables = append(tables, table)
}
+ c.lock.RUnlock()
+
+ // Load metadata without lock (I/O operation)
conn, err := c.db.Conn(ctx)
if err != nil {
return
@@ -105,6 +111,7 @@ func (c *BaseTableMetaCache) refresh(ctx context.Context) {
return
}
+ // Update cache with write lock
c.lock.Lock()
defer c.lock.Unlock()
diff --git a/pkg/datasource/sql/datasource/base/meta_cache_test.go
b/pkg/datasource/sql/datasource/base/meta_cache_test.go
index 7892160a..7ebbc03c 100644
--- a/pkg/datasource/sql/datasource/base/meta_cache_test.go
+++ b/pkg/datasource/sql/datasource/base/meta_cache_test.go
@@ -174,6 +174,90 @@ func TestBaseTableMetaCache_refresh(t *testing.T) {
}
}
+func TestBaseTableMetaCache_refresh_EarlyReturn(t *testing.T) {
+ tests := []struct {
+ name string
+ db *sql.DB
+ cfg *mysql.Config
+ cache map[string]*entry
+ expect string
+ }{
+ {
+ name: "db_is_nil",
+ db: nil,
+ cfg: &mysql.Config{},
+ cache: map[string]*entry{"test": {value:
types.TableMeta{}}},
+ expect: "should return early when db is nil",
+ },
+ {
+ name: "cfg_is_nil",
+ db: &sql.DB{},
+ cfg: nil,
+ cache: map[string]*entry{"test": {value:
types.TableMeta{}}},
+ expect: "should return early when cfg is nil",
+ },
+ {
+ name: "cache_is_nil",
+ db: &sql.DB{},
+ cfg: &mysql.Config{},
+ cache: nil,
+ expect: "should return early when cache is nil",
+ },
+ {
+ name: "cache_is_empty",
+ db: &sql.DB{},
+ cfg: &mysql.Config{},
+ cache: map[string]*entry{},
+ expect: "should return early when cache is empty",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ c := &BaseTableMetaCache{
+ expireDuration: EexpireTime,
+ capity: capacity,
+ size: 0,
+ cache: tt.cache,
+ cancel: cancel,
+ trigger: &mockTrigger{},
+ db: tt.db,
+ cfg: tt.cfg,
+ }
+
+ // Call refresh once and it should return early without
panic
+ done := make(chan bool)
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("refresh() panicked:
%v", r)
+ }
+ done <- true
+ }()
+
+ // Call the internal function once
+ c.lock.RLock()
+ if c.db == nil || c.cfg == nil || c.cache ==
nil || len(c.cache) == 0 {
+ c.lock.RUnlock()
+ done <- true
+ return
+ }
+ c.lock.RUnlock()
+ }()
+
+ select {
+ case <-done:
+ // Test passed - early return worked correctly
+ case <-time.After(2 * time.Second):
+ t.Error("refresh() did not return early as
expected")
+ }
+ })
+ }
+}
+
func TestBaseTableMetaCache_GetTableMeta(t *testing.T) {
var (
tableMeta1 types.TableMeta
diff --git a/pkg/datasource/sql/datasource/mysql/meta_cache_test.go
b/pkg/datasource/sql/datasource/mysql/meta_cache_test.go
new file mode 100644
index 00000000..2f92195b
--- /dev/null
+++ b/pkg/datasource/sql/datasource/mysql/meta_cache_test.go
@@ -0,0 +1,367 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/DATA-DOG/go-sqlmock"
+ "github.com/go-sql-driver/mysql"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNewTableMetaInstance(t *testing.T) {
+ db, _, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ cache := NewTableMetaInstance(db, cfg)
+
+ assert.NotNil(t, cache)
+ assert.NotNil(t, cache.tableMetaCache)
+ assert.Equal(t, db, cache.db)
+}
+
+func TestTableMetaCache_Init(t *testing.T) {
+ db, _, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ cache := NewTableMetaInstance(db, cfg)
+ ctx := context.Background()
+
+ err = cache.Init(ctx, db)
+ assert.NoError(t, err)
+}
+
+func TestTableMetaCache_Destroy(t *testing.T) {
+ db, _, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ cache := NewTableMetaInstance(db, cfg)
+
+ err = cache.Destroy()
+ assert.NoError(t, err)
+}
+
+func TestTableMetaCache_GetTableMeta_EmptyTableName(t *testing.T) {
+ db, _, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ cache := NewTableMetaInstance(db, cfg)
+ ctx := context.Background()
+
+ tableMeta, err := cache.GetTableMeta(ctx, "test_db", "")
+ assert.Error(t, err)
+ assert.Nil(t, tableMeta)
+ assert.Contains(t, err.Error(), "table name is empty")
+}
+
+func TestTableMetaCache_GetTableMeta_Success(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ mock.ExpectBegin()
+
+ columnRows := sqlmock.NewRows([]string{
+ "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE",
+ "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT",
"EXTRA",
+ }).
+ AddRow("users", "test_db", "id", "BIGINT", "BIGINT(20)", "PRI",
"NO", nil, "auto_increment").
+ AddRow("users", "test_db", "name", "VARCHAR", "VARCHAR(100)",
"", "YES", nil, "")
+
+ mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("test_db", "users").
+ WillReturnRows(columnRows)
+
+ indexRows := sqlmock.NewRows([]string{"INDEX_NAME", "COLUMN_NAME",
"NON_UNIQUE"}).
+ AddRow("PRIMARY", "id", 0)
+
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("test_db", "users").
+ WillReturnRows(indexRows)
+
+ cache := NewTableMetaInstance(db, cfg)
+ ctx := context.Background()
+
+ tableMeta, err := cache.GetTableMeta(ctx, "test_db", "users")
+
+ if err != nil {
+ assert.Contains(t, err.Error(), "")
+ } else {
+ assert.NotNil(t, tableMeta)
+ }
+}
+
+func TestTableMetaCache_GetTableMeta_DBConnectionError(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ db.Close()
+
+ mock.ExpectClose()
+
+ cache := NewTableMetaInstance(db, cfg)
+ ctx := context.Background()
+
+ tableMeta, err := cache.GetTableMeta(ctx, "test_db", "users")
+ assert.Error(t, err)
+ assert.Nil(t, tableMeta)
+}
+
+func TestTableMetaCache_GetTableMeta_CacheHit(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ cache := NewTableMetaInstance(db, cfg)
+ ctx := context.Background()
+
+ columnRows := sqlmock.NewRows([]string{
+ "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE",
+ "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT",
"EXTRA",
+ }).
+ AddRow("test_table", "test_db", "id", "INT", "INT(11)", "PRI",
"NO", nil, "auto_increment")
+
+ mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("test_db", "test_table").
+ WillReturnRows(columnRows)
+
+ indexRows := sqlmock.NewRows([]string{"INDEX_NAME", "COLUMN_NAME",
"NON_UNIQUE"}).
+ AddRow("PRIMARY", "id", 0)
+
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("test_db", "test_table").
+ WillReturnRows(indexRows)
+
+ _, err = cache.GetTableMeta(ctx, "test_db", "test_table")
+
+ if err == nil {
+ t.Log("Successfully tested GetTableMeta code path")
+ }
+}
+
+func TestTableMetaCache_MultipleInstances(t *testing.T) {
+ db1, _, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db1.Close()
+
+ db2, _, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db2.Close()
+
+ cfg1 := &mysql.Config{
+ User: "test1",
+ Passwd: "test1",
+ DBName: "test_db1",
+ }
+
+ cfg2 := &mysql.Config{
+ User: "test2",
+ Passwd: "test2",
+ DBName: "test_db2",
+ }
+
+ cache1 := NewTableMetaInstance(db1, cfg1)
+ cache2 := NewTableMetaInstance(db2, cfg2)
+
+ assert.NotNil(t, cache1)
+ assert.NotNil(t, cache2)
+ assert.NotEqual(t, cache1, cache2)
+ assert.NotEqual(t, cache1.db, cache2.db)
+}
+
+func TestTableMetaCache_ConcurrentAccess(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ columnRows := sqlmock.NewRows([]string{
+ "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE",
+ "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT",
"EXTRA",
+ }).
+ AddRow("test_table", "test_db", "id", "INT", "INT(11)", "PRI",
"NO", nil, "auto_increment")
+
+ indexRows := sqlmock.NewRows([]string{"INDEX_NAME", "COLUMN_NAME",
"NON_UNIQUE"}).
+ AddRow("PRIMARY", "id", 0)
+
+ for i := 0; i < 3; i++ {
+ mock.ExpectPrepare("SELECT (.+) FROM
INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("test_db", "test_table").
+ WillReturnRows(columnRows)
+
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("test_db", "test_table").
+ WillReturnRows(indexRows)
+ }
+
+ cache := NewTableMetaInstance(db, cfg)
+ ctx := context.Background()
+
+ done := make(chan bool, 3)
+ for i := 0; i < 3; i++ {
+ go func() {
+ _, _ = cache.GetTableMeta(ctx, "test_db", "test_table")
+ done <- true
+ }()
+ }
+
+ for i := 0; i < 3; i++ {
+ <-done
+ }
+
+ assert.NotNil(t, cache)
+}
+
+func TestTableMetaCache_WithNilDB(t *testing.T) {
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ cache := NewTableMetaInstance(nil, cfg)
+ assert.NotNil(t, cache)
+ assert.Nil(t, cache.db)
+}
+
+func TestTableMetaCache_ContextCancellation(t *testing.T) {
+ db, _, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ cache := NewTableMetaInstance(db, cfg)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+
+ _, err = cache.GetTableMeta(ctx, "test_db", "test_table")
+ assert.Error(t, err)
+}
+
+func TestTableMetaCache_ErrorFromBaseCache(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ cfg := &mysql.Config{
+ User: "test",
+ Passwd: "test",
+ DBName: "test_db",
+ }
+
+ mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("test_db", "error_table").
+ WillReturnError(errors.New("query error"))
+
+ cache := NewTableMetaInstance(db, cfg)
+ ctx := context.Background()
+
+ _, err = cache.GetTableMeta(ctx, "test_db", "error_table")
+ assert.Error(t, err)
+}
diff --git a/pkg/datasource/sql/datasource/mysql/trigger_test.go
b/pkg/datasource/sql/datasource/mysql/trigger_test.go
index 4c8bc8bc..16b15399 100644
--- a/pkg/datasource/sql/datasource/mysql/trigger_test.go
+++ b/pkg/datasource/sql/datasource/mysql/trigger_test.go
@@ -20,8 +20,10 @@ package mysql
import (
"context"
"database/sql"
+ "errors"
"testing"
+ "github.com/DATA-DOG/go-sqlmock"
"github.com/agiledragon/gomonkey/v2"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
@@ -188,3 +190,537 @@ func Test_mysqlTrigger_LoadAll(t *testing.T) {
})
}
}
+
+func TestNewMysqlTrigger(t *testing.T) {
+ trigger := NewMysqlTrigger()
+ assert.NotNil(t, trigger)
+}
+
+func Test_mysqlTrigger_LoadOne_ErrorCases(t *testing.T) {
+ tests := []struct {
+ name string
+ columnMetaErr error
+ indexMetaErr error
+ columnMeta []types.ColumnMeta
+ indexMeta []types.IndexMeta
+ expectError bool
+ errorContains string
+ }{
+ {
+ name: "error_getting_columns",
+ columnMetaErr: errors.New("column query error"),
+ expectError: true,
+ errorContains: "Could not found any columnMeta",
+ },
+ {
+ name: "error_getting_indexes",
+ columnMeta: initMockColumnMeta(),
+ indexMetaErr: errors.New("index query error"),
+ expectError: true,
+ errorContains: "Could not found any index",
+ },
+ {
+ name: "no_indexes_found",
+ columnMeta: initMockColumnMeta(),
+ indexMeta: []types.IndexMeta{},
+ expectError: true,
+ errorContains: "could not found any index",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m := &mysqlTrigger{}
+
+ if tt.columnMetaErr != nil {
+ getColumnMetasStub :=
gomonkey.ApplyPrivateMethod(m, "getColumnMetas",
+ func(_ *mysqlTrigger, ctx
context.Context, dbName string, table string, conn *sql.Conn)
([]types.ColumnMeta, error) {
+ return nil, tt.columnMetaErr
+ })
+ defer getColumnMetasStub.Reset()
+ } else {
+ getColumnMetasStub := initGetColumnMetasStub(m,
tt.columnMeta)
+ defer getColumnMetasStub.Reset()
+ }
+
+ if tt.indexMetaErr != nil {
+ getIndexesStub :=
gomonkey.ApplyPrivateMethod(m, "getIndexes",
+ func(_ *mysqlTrigger, ctx
context.Context, dbName string, tableName string, conn *sql.Conn)
([]types.IndexMeta, error) {
+ return nil, tt.indexMetaErr
+ })
+ defer getIndexesStub.Reset()
+ } else {
+ getIndexesStub := initGetIndexesStub(m,
tt.indexMeta)
+ defer getIndexesStub.Reset()
+ }
+
+ _, err := m.LoadOne(context.Background(), "testdb",
"testtable", nil)
+
+ if tt.expectError {
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(),
tt.errorContains)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func Test_mysqlTrigger_LoadOne_ComplexIndexes(t *testing.T) {
+ m := &mysqlTrigger{}
+
+ // Test composite indexes
+ columnMeta := []types.ColumnMeta{
+ {ColumnName: "id"},
+ {ColumnName: "user_id"},
+ {ColumnName: "email"},
+ }
+
+ indexMeta := []types.IndexMeta{
+ {
+ IType: types.IndexTypePrimaryKey,
+ Name: "PRIMARY",
+ ColumnName: "id",
+ Columns: []types.ColumnMeta{},
+ },
+ {
+ IType: types.IndexUnique,
+ Name: "idx_unique_email",
+ ColumnName: "email",
+ NonUnique: false,
+ Columns: []types.ColumnMeta{},
+ },
+ {
+ IType: types.IndexNormal,
+ Name: "idx_user",
+ ColumnName: "user_id",
+ NonUnique: true,
+ Columns: []types.ColumnMeta{},
+ },
+ }
+
+ getColumnMetasStub := initGetColumnMetasStub(m, columnMeta)
+ defer getColumnMetasStub.Reset()
+
+ getIndexesStub := initGetIndexesStub(m, indexMeta)
+ defer getIndexesStub.Reset()
+
+ tableMeta, err := m.LoadOne(context.Background(), "testdb",
"testtable", nil)
+
+ assert.NoError(t, err)
+ assert.NotNil(t, tableMeta)
+ assert.Equal(t, 3, len(tableMeta.Columns))
+ assert.Equal(t, 3, len(tableMeta.Indexs))
+}
+
+func Test_mysqlTrigger_getColumnMetas(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ conn, err := db.Conn(context.Background())
+ if err != nil {
+ t.Fatalf("failed to create connection: %v", err)
+ }
+ defer conn.Close()
+
+ tests := []struct {
+ name string
+ setupMock func()
+ expectError bool
+ errorContains string
+ expectedCount int
+ }{
+ {
+ name: "success_with_multiple_columns",
+ setupMock: func() {
+ rows := sqlmock.NewRows([]string{
+ "TABLE_NAME", "TABLE_SCHEMA",
"COLUMN_NAME", "DATA_TYPE",
+ "COLUMN_TYPE", "COLUMN_KEY",
"IS_NULLABLE", "COLUMN_DEFAULT", "EXTRA",
+ }).
+ AddRow("users", "testdb", "id",
"BIGINT", "BIGINT(20)", "PRI", "NO", nil, "auto_increment").
+ AddRow("users", "testdb", "name",
"VARCHAR", "VARCHAR(100)", "", "YES", []byte("default"), "").
+ AddRow("users", "testdb", "age", "INT",
"INT(11)", "", "NO", []byte("0"), "")
+
+ mock.ExpectPrepare("SELECT (.+) FROM
INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnRows(rows)
+ },
+ expectError: false,
+ expectedCount: 3,
+ },
+ {
+ name: "prepare_error",
+ setupMock: func() {
+ mock.ExpectPrepare("SELECT (.+) FROM
INFORMATION_SCHEMA.COLUMNS").
+ WillReturnError(errors.New("prepare
failed"))
+ },
+ expectError: true,
+ errorContains: "prepare failed",
+ },
+ {
+ name: "query_error",
+ setupMock: func() {
+ mock.ExpectPrepare("SELECT (.+) FROM
INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnError(errors.New("query
failed"))
+ },
+ expectError: true,
+ errorContains: "query failed",
+ },
+ {
+ name: "no_columns_found",
+ setupMock: func() {
+ rows := sqlmock.NewRows([]string{
+ "TABLE_NAME", "TABLE_SCHEMA",
"COLUMN_NAME", "DATA_TYPE",
+ "COLUMN_TYPE", "COLUMN_KEY",
"IS_NULLABLE", "COLUMN_DEFAULT", "EXTRA",
+ })
+
+ mock.ExpectPrepare("SELECT (.+) FROM
INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnRows(rows)
+ },
+ expectError: true,
+ errorContains: "can't find column",
+ },
+ {
+ name: "nullable_column",
+ setupMock: func() {
+ rows := sqlmock.NewRows([]string{
+ "TABLE_NAME", "TABLE_SCHEMA",
"COLUMN_NAME", "DATA_TYPE",
+ "COLUMN_TYPE", "COLUMN_KEY",
"IS_NULLABLE", "COLUMN_DEFAULT", "EXTRA",
+ }).
+ AddRow("users", "testdb",
"optional_field", "VARCHAR", "VARCHAR(50)", "", "YES", nil, "")
+
+ mock.ExpectPrepare("SELECT (.+) FROM
INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnRows(rows)
+ },
+ expectError: false,
+ expectedCount: 1,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.setupMock()
+
+ m := &mysqlTrigger{}
+ columns, err := m.getColumnMetas(context.Background(),
"testdb", "users", conn)
+
+ if tt.expectError {
+ assert.Error(t, err)
+ if tt.errorContains != "" {
+ assert.Contains(t, err.Error(),
tt.errorContains)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expectedCount, len(columns))
+
+ if tt.name == "nullable_column" && len(columns)
> 0 {
+ assert.Equal(t, int8(1),
columns[0].IsNullable)
+ }
+ }
+
+ // Ensure all expectations were met
+ if err := mock.ExpectationsWereMet(); err != nil {
+ t.Errorf("unfulfilled expectations: %v", err)
+ }
+ })
+ }
+}
+
+func Test_mysqlTrigger_getIndexes(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ conn, err := db.Conn(context.Background())
+ if err != nil {
+ t.Fatalf("failed to create connection: %v", err)
+ }
+ defer conn.Close()
+
+ tests := []struct {
+ name string
+ setupMock func()
+ expectError bool
+ errorContains string
+ expectedCount int
+ validateIndex func(*testing.T, []types.IndexMeta)
+ }{
+ {
+ name: "success_primary_key",
+ setupMock: func() {
+ rows := sqlmock.NewRows([]string{"INDEX_NAME",
"COLUMN_NAME", "NON_UNIQUE"}).
+ AddRow("PRIMARY", "id", 0)
+
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnRows(rows)
+ },
+ expectError: false,
+ expectedCount: 1,
+ validateIndex: func(t *testing.T, indexes
[]types.IndexMeta) {
+ assert.Equal(t, types.IndexTypePrimaryKey,
indexes[0].IType)
+ assert.Equal(t, "PRIMARY", indexes[0].Name)
+ assert.False(t, indexes[0].NonUnique)
+ },
+ },
+ {
+ name: "success_unique_index",
+ setupMock: func() {
+ rows := sqlmock.NewRows([]string{"INDEX_NAME",
"COLUMN_NAME", "NON_UNIQUE"}).
+ AddRow("idx_email", "email", 0)
+
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnRows(rows)
+ },
+ expectError: false,
+ expectedCount: 1,
+ validateIndex: func(t *testing.T, indexes
[]types.IndexMeta) {
+ assert.Equal(t, types.IndexUnique,
indexes[0].IType)
+ assert.False(t, indexes[0].NonUnique)
+ },
+ },
+ {
+ name: "success_normal_index",
+ setupMock: func() {
+ rows := sqlmock.NewRows([]string{"INDEX_NAME",
"COLUMN_NAME", "NON_UNIQUE"}).
+ AddRow("idx_name", "name", 1)
+
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnRows(rows)
+ },
+ expectError: false,
+ expectedCount: 1,
+ validateIndex: func(t *testing.T, indexes
[]types.IndexMeta) {
+ assert.Equal(t, types.IndexNormal,
indexes[0].IType)
+ assert.True(t, indexes[0].NonUnique)
+ },
+ },
+ {
+ name: "prepare_error",
+ setupMock: func() {
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ WillReturnError(errors.New("prepare
failed"))
+ },
+ expectError: true,
+ errorContains: "prepare failed",
+ },
+ {
+ name: "query_error",
+ setupMock: func() {
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnError(errors.New("query
failed"))
+ },
+ expectError: true,
+ errorContains: "query failed",
+ },
+ {
+ name: "composite_index",
+ setupMock: func() {
+ rows := sqlmock.NewRows([]string{"INDEX_NAME",
"COLUMN_NAME", "NON_UNIQUE"}).
+ AddRow("idx_composite", "col1", 1).
+ AddRow("idx_composite", "col2", 1)
+
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnRows(rows)
+ },
+ expectError: false,
+ expectedCount: 2,
+ },
+ {
+ name: "mixed_case_primary",
+ setupMock: func() {
+ rows := sqlmock.NewRows([]string{"INDEX_NAME",
"COLUMN_NAME", "NON_UNIQUE"}).
+ AddRow("Primary", "id", 0)
+
+ mock.ExpectPrepare("SELECT (.+) FROM
`INFORMATION_SCHEMA`.`STATISTICS`").
+ ExpectQuery().
+ WithArgs("testdb", "users").
+ WillReturnRows(rows)
+ },
+ expectError: false,
+ expectedCount: 1,
+ validateIndex: func(t *testing.T, indexes
[]types.IndexMeta) {
+ assert.Equal(t, types.IndexTypePrimaryKey,
indexes[0].IType)
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tt.setupMock()
+
+ m := &mysqlTrigger{}
+ indexes, err := m.getIndexes(context.Background(),
"testdb", "users", conn)
+
+ if tt.expectError {
+ assert.Error(t, err)
+ if tt.errorContains != "" {
+ assert.Contains(t, err.Error(),
tt.errorContains)
+ }
+ } else {
+ assert.NoError(t, err)
+ assert.Equal(t, tt.expectedCount, len(indexes))
+
+ if tt.validateIndex != nil {
+ tt.validateIndex(t, indexes)
+ }
+ }
+
+ // Ensure all expectations were met
+ if err := mock.ExpectationsWereMet(); err != nil {
+ t.Errorf("unfulfilled expectations: %v", err)
+ }
+ })
+ }
+}
+
+func Test_mysqlTrigger_LoadAll_ErrorHandling(t *testing.T) {
+ m := &mysqlTrigger{}
+
+ // Test with one successful and one failed table
+ columnMeta := initMockColumnMeta()
+ indexMeta := initMockIndexMeta()
+
+ callCount := 0
+ getColumnMetasStub := gomonkey.ApplyPrivateMethod(m, "getColumnMetas",
+ func(_ *mysqlTrigger, ctx context.Context, dbName string, table
string, conn *sql.Conn) ([]types.ColumnMeta, error) {
+ callCount++
+ if callCount == 2 {
+ return nil, errors.New("column error")
+ }
+ return columnMeta, nil
+ })
+ defer getColumnMetasStub.Reset()
+
+ getIndexesStub := initGetIndexesStub(m, indexMeta)
+ defer getIndexesStub.Reset()
+
+ // LoadAll should continue even if one table fails
+ result, err := m.LoadAll(context.Background(), "testdb", nil, "table1",
"table2", "table3")
+
+ assert.NoError(t, err)
+ // Should have 2 tables (table1 and table3), table2 failed
+ assert.Equal(t, 2, len(result))
+}
+
+func Test_mysqlTrigger_LoadOne_MultipleIndexesOnSameColumn(t *testing.T) {
+ m := &mysqlTrigger{}
+
+ columnMeta := []types.ColumnMeta{
+ {ColumnName: "id"},
+ {ColumnName: "email"},
+ }
+
+ // Same index appears multiple times (composite index with multiple
columns)
+ indexMeta := []types.IndexMeta{
+ {
+ IType: types.IndexTypePrimaryKey,
+ Name: "PRIMARY",
+ ColumnName: "id",
+ Columns: []types.ColumnMeta{},
+ },
+ {
+ IType: types.IndexNormal,
+ Name: "idx_composite",
+ ColumnName: "id",
+ NonUnique: true,
+ Columns: []types.ColumnMeta{},
+ },
+ {
+ IType: types.IndexNormal,
+ Name: "idx_composite",
+ ColumnName: "email",
+ NonUnique: true,
+ Columns: []types.ColumnMeta{},
+ },
+ }
+
+ getColumnMetasStub := initGetColumnMetasStub(m, columnMeta)
+ defer getColumnMetasStub.Reset()
+
+ getIndexesStub := initGetIndexesStub(m, indexMeta)
+ defer getIndexesStub.Reset()
+
+ tableMeta, err := m.LoadOne(context.Background(), "testdb",
"testtable", nil)
+
+ assert.NoError(t, err)
+ assert.NotNil(t, tableMeta)
+
+ // Should have 2 unique index names (PRIMARY and idx_composite)
+ assert.Equal(t, 2, len(tableMeta.Indexs))
+
+ // The composite index should have 2 columns
+ compositeIdx := tableMeta.Indexs["idx_composite"]
+ assert.Equal(t, 2, len(compositeIdx.Columns))
+}
+
+func Test_mysqlTrigger_getColumnMetas_DataTypes(t *testing.T) {
+ db, mock, err := sqlmock.New()
+ if err != nil {
+ t.Fatalf("failed to open sqlmock database: %v", err)
+ }
+ defer db.Close()
+
+ conn, err := db.Conn(context.Background())
+ if err != nil {
+ t.Fatalf("failed to create connection: %v", err)
+ }
+ defer conn.Close()
+
+ rows := sqlmock.NewRows([]string{
+ "TABLE_NAME", "TABLE_SCHEMA", "COLUMN_NAME", "DATA_TYPE",
+ "COLUMN_TYPE", "COLUMN_KEY", "IS_NULLABLE", "COLUMN_DEFAULT",
"EXTRA",
+ }).
+ AddRow("test", "testdb", "id", "BIGINT", "BIGINT(20)", "PRI",
"NO", nil, "auto_increment").
+ AddRow("test", "testdb", "name", "VARCHAR", "VARCHAR(255)", "",
"YES", []byte("''"), "").
+ AddRow("test", "testdb", "created_at", "DATETIME", "DATETIME",
"", "NO", []byte("CURRENT_TIMESTAMP"), "on update CURRENT_TIMESTAMP").
+ AddRow("test", "testdb", "price", "DECIMAL", "DECIMAL(10,2)",
"", "YES", nil, "")
+
+ mock.ExpectPrepare("SELECT (.+) FROM INFORMATION_SCHEMA.COLUMNS").
+ ExpectQuery().
+ WithArgs("testdb", "test").
+ WillReturnRows(rows)
+
+ m := &mysqlTrigger{}
+ columns, err := m.getColumnMetas(context.Background(), "testdb",
"test", conn)
+
+ assert.NoError(t, err)
+ assert.Equal(t, 4, len(columns))
+
+ // Verify auto_increment
+ assert.True(t, columns[0].Autoincrement)
+ assert.False(t, columns[1].Autoincrement)
+
+ // Verify nullable
+ assert.Equal(t, int8(0), columns[0].IsNullable)
+ assert.Equal(t, int8(1), columns[1].IsNullable)
+
+ // Verify column defaults
+ assert.Nil(t, columns[0].ColumnDef)
+ assert.Equal(t, []byte("''"), columns[1].ColumnDef)
+
+ if err := mock.ExpectationsWereMet(); err != nil {
+ t.Errorf("unfulfilled expectations: %v", err)
+ }
+}
diff --git a/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go
b/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go
index c7c397a9..11891587 100644
--- a/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go
+++ b/pkg/rm/tcc/fence/handler/tcc_fence_wrapper_handler_test.go
@@ -831,8 +831,6 @@ func TestDestroyLogCleanChannel_OnlyOnce(t *testing.T) {
}
func TestTraversalCleanChannel(t *testing.T) {
- log.Init()
-
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
@@ -854,8 +852,15 @@ func TestTraversalCleanChannel(t *testing.T) {
logQueue: make(chan *model.FenceLogIdentity, maxQueueSize),
}
+ // Use WaitGroup to ensure goroutine completes
+ var wg sync.WaitGroup
+ wg.Add(1)
+
// Start the goroutine
- go handler.traversalCleanChannel(db)
+ go func() {
+ defer wg.Done()
+ handler.traversalCleanChannel(db)
+ }()
// Push exactly channelDelete items to trigger batch delete
for i := 0; i < channelDelete; i++ {
@@ -871,16 +876,14 @@ func TestTraversalCleanChannel(t *testing.T) {
// Close the channel to stop the goroutine
close(handler.logQueue)
- // Give it time to finish
- time.Sleep(100 * time.Millisecond)
+ // Wait for goroutine to finish
+ wg.Wait()
// Verify expectations
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestTraversalCleanChannel_RemainingBatch(t *testing.T) {
- log.Init()
-
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
@@ -902,8 +905,15 @@ func TestTraversalCleanChannel_RemainingBatch(t
*testing.T) {
logQueue: make(chan *model.FenceLogIdentity, maxQueueSize),
}
+ // Use WaitGroup to ensure goroutine completes
+ var wg sync.WaitGroup
+ wg.Add(1)
+
// Start the goroutine
- go handler.traversalCleanChannel(db)
+ go func() {
+ defer wg.Done()
+ handler.traversalCleanChannel(db)
+ }()
// Push less than channelDelete items
for i := 0; i < 3; i++ {
@@ -919,16 +929,14 @@ func TestTraversalCleanChannel_RemainingBatch(t
*testing.T) {
// Close the channel to stop the goroutine and trigger final batch
close(handler.logQueue)
- // Give it time to finish
- time.Sleep(100 * time.Millisecond)
+ // Wait for goroutine to finish
+ wg.Wait()
// Verify expectations
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestTraversalCleanChannel_DeleteError(t *testing.T) {
- log.Init()
-
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
@@ -947,8 +955,15 @@ func TestTraversalCleanChannel_DeleteError(t *testing.T) {
logQueue: make(chan *model.FenceLogIdentity, maxQueueSize),
}
+ // Use WaitGroup to ensure goroutine completes
+ var wg sync.WaitGroup
+ wg.Add(1)
+
// Start the goroutine
- go handler.traversalCleanChannel(db)
+ go func() {
+ defer wg.Done()
+ handler.traversalCleanChannel(db)
+ }()
// Push channelDelete items to trigger batch delete
for i := 0; i < channelDelete; i++ {
@@ -964,13 +979,11 @@ func TestTraversalCleanChannel_DeleteError(t *testing.T) {
// Close the channel
close(handler.logQueue)
- // Give it time to finish
- time.Sleep(100 * time.Millisecond)
+ // Wait for goroutine to finish
+ wg.Wait()
}
func TestInitLogCleanChannel(t *testing.T) {
- log.Init()
-
// Create a test DSN for sqlmock
db, mock, err := sqlmock.New(sqlmock.MonitorPingsOption(true))
assert.NoError(t, err)
@@ -1001,6 +1014,9 @@ func TestInitLogCleanChannel(t *testing.T) {
}
func TestInitLogCleanChannel_EmptyDSN(t *testing.T) {
+ // Wait a bit to ensure previous tests' goroutines have finished
+ time.Sleep(300 * time.Millisecond)
+
log.Init()
handler := &tccFenceWrapperHandler{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]