Files
goauto/server/cmd/migrate/migration/init_test.go
T

143 lines
4.4 KiB
Go

package migration
import (
"fmt"
"log"
"os"
"regexp"
"strings"
"testing"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type migrationRow struct {
ID uint `gorm:"primaryKey"`
Version string `gorm:"size:255"`
}
func (migrationRow) TableName() string { return "sys_migration" }
// captureOutput 同时捕获 stdout 和标准 log 的输出。
//
// `[必须]` 必须包含 log:本来要防的那个回归(每个迁移打一个裸 "1")走的正是
// log 的默认输出 stderr,只盯 stdout 的断言在它面前是空转的。而且 stderr 才是
// 启动脚本 `2>&1 |` 变成红色错误块的那一路。
func captureOutput(t *testing.T, run func()) string {
t.Helper()
originalStdout, originalLogOutput := os.Stdout, log.Writer()
reader, writer, err := os.Pipe()
if err != nil {
t.Fatalf("建管道失败: %v", err)
}
os.Stdout = writer
log.SetOutput(writer)
run()
writer.Close()
os.Stdout = originalStdout
log.SetOutput(originalLogOutput)
var builder strings.Builder
buffer := make([]byte, 4096)
for {
n, err := reader.Read(buffer)
builder.Write(buffer[:n])
if err != nil {
break
}
}
return builder.String()
}
func newMigrationTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"),
&gorm.Config{Logger: logger.Default.LogMode(logger.Info)})
if err != nil {
t.Fatalf("打开数据库失败: %v", err)
}
if err := db.AutoMigrate(&migrationRow{}); err != nil {
t.Fatalf("建表失败: %v", err)
}
return db
}
// 已应用的迁移只汇总成一行,不再每个打印一个裸 "1"。
func TestMigrateSummarisesInsteadOfPrintingBareCounts(t *testing.T) {
db := newMigrationTestDB(t)
db.Create(&migrationRow{Version: "0000000000001"})
db.Create(&migrationRow{Version: "0000000000002"})
migration := &Migration{version: map[string]func(*gorm.DB, string) error{}}
migration.SetDb(db)
for _, version := range []string{"0000000000001", "0000000000002"} {
migration.SetVersion(version, func(*gorm.DB, string) error { return nil })
}
output := captureOutput(t, migration.Migrate)
if !strings.Contains(output, "跳过 2 个") {
t.Fatalf("应汇总跳过数量: %q", output)
}
if !strings.Contains(output, "新执行 0 个") {
t.Fatalf("应汇总新执行数量: %q", output)
}
// 上游那些孤零零的计数行不该再出现。
//
// `[必须]` 匹配时必须允许 log 的时间戳前缀。实际输出是
// "2026/08/20 09:48:41 1",拿 == "1" 去比永远不成立——这条断言最初就是
// 这么写的,变异测试才发现它根本不会失败。
bareCount := regexp.MustCompile(`(?m)^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} )?\d+\s*$`)
if bareCount.MatchString(output) {
t.Fatalf("仍在打印裸计数: %q", output)
}
// 存在性检查用 Warn 级会话,不该回显 SELECT。
if strings.Contains(output, "SELECT count(*)") {
t.Fatalf("版本存在性检查不应回显 SQL: %q", output)
}
}
// 没跑过的迁移要真的执行,并计入汇总。
func TestMigrateRunsPendingVersions(t *testing.T) {
db := newMigrationTestDB(t)
db.Create(&migrationRow{Version: "0000000000001"})
executed := make([]string, 0, 2)
migration := &Migration{version: map[string]func(*gorm.DB, string) error{}}
migration.SetDb(db)
migration.SetVersion("0000000000001", func(*gorm.DB, string) error {
executed = append(executed, "one")
return nil
})
migration.SetVersion("0000000000002", func(*gorm.DB, string) error {
executed = append(executed, "two")
return nil
})
output := captureOutput(t, migration.Migrate)
if len(executed) != 1 || executed[0] != "two" {
t.Fatalf("只应执行未应用的版本,实际执行 %v", executed)
}
if !strings.Contains(output, "新执行 1 个") || !strings.Contains(output, "跳过 1 个") {
t.Fatalf("汇总不对: %q", output)
}
}
func TestSetVersionRejectsDuplicateRegistration(t *testing.T) {
migration := &Migration{version: map[string]func(*gorm.DB, string) error{}}
migration.SetVersion("0000000000001", func(*gorm.DB, string) error { return nil })
defer func() {
value := recover()
if value == nil {
t.Fatal("重复迁移版本不应静默覆盖")
}
if !strings.Contains(fmt.Sprint(value), "duplicate migration version registered: 0000000000001") {
t.Fatalf("重复版本错误不明确: %v", value)
}
}()
migration.SetVersion("0000000000001", func(*gorm.DB, string) error { return nil })
}