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 }) }