go-study/query_builder/service_test.go
fantasticbin 5b48ea1a62 使用codex优化代码,具体如下:
主要优化

  - err_group:去掉多余 goroutine,避免潜在泄漏;并把并发 append 改为按下标写
    入,消除数据竞争。
  - err_group 测试稳定性增强:放宽超时并增加结果长度断言。
  - semaphore:修复等待队列元素类型断言错误(*waiter);补充非法参数校验(负数
    acquire/release)。
  - SemaChan:修复 Lock/Unlock 逻辑(初始化令牌桶),避免永久阻塞。
  - observer:修复“每次 Notify 都启动新 fanout 协程”的问题:改为 sync.Once 只启动一次
    fanOut。
  - observer:修复并发读写观察者列表问题:给 Attach/Detach/fanOut 增加读写锁保护。
  - observer:去掉 fanout 内部额外再起 goroutine和自动关闭所有 observer 的行为,避
    免重复关闭/竞态风险(仍保留 Detach 时关闭单个 observer)。
  - lock_free:修复可取消延迟队列的计数错误与 timers map 并发访问问题。
  - lock_free:checkAckStatus 改为非阻塞读取,避免入队路径被卡住。
  - routine:提供默认空任务并忽略 nil taskFn,防止空指针调用。
  - ticker:发送改为非阻塞,Stop 幂等化,降低阻塞和重复关闭风险。
  - query_builder:WaitAndGo 增加 goroutine 内 panic 转 error;测试里
    的 GORM filter 链式写法修正。

  新增测试

  - 新增 semaphore 测试,覆盖 Acquire/Release/TryAcquire 与 SemaChan 并发上限。
2026-03-05 21:53:11 +08:00

230 lines
5.8 KiB
Go

package builder
import (
"context"
"errors"
"fmt"
"testing"
"time"
"go.uber.org/mock/gomock"
"gorm.io/gorm"
)
type TestEntity struct {
ID uint32
Name string
Age int
}
type TestFilter struct {
Name string
Age uint8
}
type TestSort struct {
Field string
Direction string
}
type TestService struct {
filter TestFilter
sort TestSort
}
func (s *TestService) GetFilter(_ context.Context) (any, error) {
return func(db *gorm.DB) *gorm.DB {
if s.filter.Name != "" {
db = db.Where("name = ?", s.filter.Name)
}
if s.filter.Age > 0 {
db = db.Where("age >= ?", s.filter.Age)
}
return db
}, nil
}
func (s *TestService) GetSort() any {
return func(db *gorm.DB) *gorm.DB {
// 实际项目中的排序需要根据pb文件生成的枚举值来处理
return db.Order(s.sort.Field + " " + s.sort.Direction)
}
}
func TestQueryList(t *testing.T) {
ctx := context.Background()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Mock 策略实例
mockStrategy := NewMockQueryListStrategy[TestEntity](ctrl)
tests := []struct {
name string
service Service
mockSetup func()
opts []QueryOption[TestFilter, TestSort]
expectedResult []*TestEntity
expectedTotal int64
expectedErr error
}{
{
name: "无筛选查询&id升序",
service: &TestService{},
mockSetup: func() {
mockStrategy.EXPECT().
QueryList(ctx, gomock.Any()).
Return([]*TestEntity{
{ID: 1, Name: "Alice", Age: 25},
{ID: 2, Name: "Bob", Age: 30},
}, int64(2), nil)
},
opts: []QueryOption[TestFilter, TestSort]{
WithData[TestFilter, TestSort](NewDBProxy(&gorm.DB{}, nil)),
WithFilter[TestFilter, TestSort](&TestFilter{}),
WithSort[TestFilter, TestSort](TestSort{Field: "id", Direction: "asc"}),
},
expectedResult: []*TestEntity{
{ID: 1, Name: "Alice", Age: 25},
{ID: 2, Name: "Bob", Age: 30},
},
expectedTotal: 2,
expectedErr: nil,
},
{
name: "无筛选查询&age降序",
service: &TestService{},
mockSetup: func() {
mockStrategy.EXPECT().
QueryList(ctx, gomock.Any()).
Return([]*TestEntity{
{ID: 2, Name: "Bob", Age: 30},
{ID: 1, Name: "Alice", Age: 25},
}, int64(2), nil)
},
opts: []QueryOption[TestFilter, TestSort]{
WithData[TestFilter, TestSort](NewDBProxy(&gorm.DB{}, nil)),
WithFilter[TestFilter, TestSort](&TestFilter{}),
WithSort[TestFilter, TestSort](TestSort{Field: "age", Direction: "desc"}),
},
expectedResult: []*TestEntity{
{ID: 2, Name: "Bob", Age: 30},
{ID: 1, Name: "Alice", Age: 25},
},
expectedTotal: 2,
expectedErr: nil,
},
{
name: "有筛选查询",
service: &TestService{},
mockSetup: func() {
mockStrategy.EXPECT().
QueryList(ctx, gomock.Any()).
Return([]*TestEntity{
{ID: 1, Name: "Alice", Age: 25},
}, int64(1), nil)
},
opts: []QueryOption[TestFilter, TestSort]{
WithData[TestFilter, TestSort](NewDBProxy(&gorm.DB{}, nil)),
WithFilter[TestFilter, TestSort](&TestFilter{Name: "Alice"}),
WithSort[TestFilter, TestSort](TestSort{Field: "id", Direction: "desc"}),
},
expectedResult: []*TestEntity{
{ID: 1, Name: "Alice", Age: 25},
},
expectedTotal: 1,
expectedErr: nil,
},
{
name: "无数据实例",
service: &TestService{},
mockSetup: func() {
mockStrategy.EXPECT().
QueryList(ctx, gomock.Any()).
Return(nil, int64(0), nil)
},
opts: []QueryOption[TestFilter, TestSort]{
WithData[TestFilter, TestSort](NewDBProxy(&gorm.DB{}, nil)),
WithFilter[TestFilter, TestSort](&TestFilter{Name: "test"}),
WithSort[TestFilter, TestSort](TestSort{Field: "id", Direction: "asc"}),
},
expectedResult: nil,
expectedTotal: 0,
expectedErr: nil,
},
{
name: "数据实例错误",
service: &TestService{},
mockSetup: func() {
mockStrategy.EXPECT().
QueryList(ctx, gomock.Any()).
Return(nil, int64(0), errors.New("no data source provided"))
},
opts: []QueryOption[TestFilter, TestSort]{
WithFilter[TestFilter, TestSort](&TestFilter{Name: "test"}),
WithSort[TestFilter, TestSort](TestSort{Field: "id", Direction: "asc"}),
},
expectedResult: nil,
expectedTotal: 0,
expectedErr: errors.New("no data source provided"),
},
}
list := &List[TestEntity, TestFilter, TestSort]{}
// 这里使用 Mock 策略替代真实的策略
// 实际使用时会根据数据源自动选择
list.SetStrategy(mockStrategy)
// 添加耗时监控
list.Use(func(
ctx context.Context,
builder *builder[TestEntity],
next func(context.Context,
) ([]*TestEntity, int64, error)) ([]*TestEntity, int64, error) {
defer func() func() {
pre := time.Now()
return func() {
elapsed := time.Since(pre)
fmt.Println("elapsed:", elapsed)
}
}()()
result, total, err := next(ctx)
return result, total, err
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.mockSetup != nil {
tt.mockSetup()
}
result, total, err := list.Query(ctx, tt.service, tt.opts...)
if tt.expectedErr != nil {
if err == nil || err.Error() != tt.expectedErr.Error() {
t.Errorf("expected error: %v, got: %v", tt.expectedErr, err)
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if total != tt.expectedTotal {
t.Errorf("expected total: %d, got: %d", tt.expectedTotal, total)
}
if len(result) != len(tt.expectedResult) {
t.Errorf("expected result length: %d, got: %d", len(tt.expectedResult), len(result))
}
for i, item := range result {
if item.ID != tt.expectedResult[i].ID || item.Name != tt.expectedResult[i].Name || item.Age != tt.expectedResult[i].Age {
t.Errorf("expected result[%d]: %+v, got: %+v", i, tt.expectedResult[i], item)
}
}
}
})
}
}