The startup output was dominated by two upstream behaviours. Migrate printed a bare count for every already-applied migration — a lone "1" with no version, no explanation — through the standard logger, hence stderr, which the launcher's `2>&1 |` renders as a red PowerShell error block that looks like a failed migration. And the version-existence check echoed one SELECT count(*) per version. Both replaced: a Warn-level session for the check, and one summary line on stdout. Console encoding needed more than [Console]::OutputEncoding: PowerShell 5.1 decodes child-process output by the console code page, so chcp 65001 goes with it. The regression test capture was wrong twice and mutation testing caught both. It captured only stdout while the bug wrote to stderr, and it compared lines to "1" when the logger prefixes a timestamp, so the assertion could never fail. It now captures the standard logger too and matches with a pattern that allows the prefix — verified by reinstating the bug and watching the test fail. Not verified: the PowerShell edits, which need a Windows run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
3.8 KiB
Go
126 lines
3.8 KiB
Go
package migration
|
|
|
|
import (
|
|
"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)
|
|
}
|
|
}
|