Scert/backend/internal/testsupport/mysqltest/tools.go
xuans f260b221a9 修复MySQL特殊列备份并补充官方工具互操作测试
保留并验证四个待提交源码文件:显式导出可写列以保留不可见列,跳过生成列并保留仅生成列表的行数;修复扫描越界,以实际结构元数据校验恢复结果。MySQL 5.7.44与8.0.45专项回归、后端常规测试、静态检查及模块校验通过;不代表完整生产验收。
2026-09-27 20:18:43 +08:00

107 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mysqltest
import (
"context"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/go-sql-driver/mysql"
"git.pcux.cn/xuans/Scert/backend/internal/config"
)
// 官方工具仅连接本夹具创建的隔离实例,密码只传入子进程环境,不进入命令行或文件。
func (f *Server) Dump(t *testing.T, cfg config.DatabaseConfig, output string) {
t.Helper()
file, err := os.OpenFile(output, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
t.Fatal(err)
}
defer file.Close()
f.runTool(t, "mysqldump", cfg, []string{
"--single-transaction", "--no-tablespaces", "--set-gtid-purged=OFF",
"--skip-add-locks", "--hex-blob", "--complete-insert", "--skip-comments", "--skip-extended-insert",
}, nil, file)
if err := file.Close(); err != nil {
t.Fatal(err)
}
}
func (f *Server) Import(t *testing.T, cfg config.DatabaseConfig, input string) {
t.Helper()
file, err := os.Open(input)
if err != nil {
t.Fatal(err)
}
defer file.Close()
f.runTool(t, "mysql", cfg, []string{
"--binary-mode", "--batch", "--skip-reconnect", "--local-infile=0",
}, file, io.Discard)
}
func (f *Server) IsMySQL8() bool { return strings.Contains(f.version, "Ver 8.0.") }
func (f *Server) runTool(t *testing.T, name string, cfg config.DatabaseConfig, args []string, input io.Reader, output io.Writer) {
t.Helper()
parsed, err := mysql.ParseDSN(cfg.DSN)
if err != nil || cfg.Driver != "mysql" {
t.Fatal("官方工具测试连接无效")
}
if parsed.Net != "tcp" || parsed.Addr != "127.0.0.1:"+f.port || parsed.TLSConfig != f.tlsName ||
parsed.Passwd != f.password || !strings.HasPrefix(parsed.DBName, "scert_test_") || !strings.HasPrefix(parsed.User, "scert_user_") {
t.Fatal("官方工具只允许连接本次隔离实例的测试数据库")
}
binary := filepath.Join(filepath.Dir(f.binary), name)
if runtime.GOOS == "windows" {
binary += ".exe"
}
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
defer cancel()
version, err := exec.CommandContext(ctx, binary, "--no-defaults", "--version").Output()
if err != nil {
t.Fatal("无法读取官方工具版本,不能跳过互操作验证")
}
expected := "5.7."
if f.IsMySQL8() {
expected = "8.0."
}
if !strings.Contains(string(version), expected) {
t.Fatal("官方工具与本次测试服务端版本系列不一致")
}
t.Logf("官方工具:%s", strings.TrimSpace(string(version)))
base := []string{
"--no-defaults", "--protocol=TCP", "--host=127.0.0.1", "--port=" + f.port,
"--user=" + parsed.User, "--ssl-mode=VERIFY_IDENTITY", "--ssl-ca=" + filepath.Join(f.root, "server.crt"),
"--default-character-set=utf8mb4",
}
command := exec.CommandContext(ctx, binary, append(append(base, args...), parsed.DBName)...)
command.Env = mysqlToolTestEnvironment(f.root, parsed.Passwd)
command.Stdin, command.Stdout = input, output
// 错误可能含数据库内容;只报告退出状态,不透传工具的原始错误输出。
command.Stderr = io.Discard
command.WaitDelay = 2 * time.Second
if err := command.Run(); err != nil {
t.Fatalf("官方 %s 执行失败:%v;原始输出已隐藏", name, err)
}
}
func mysqlToolTestEnvironment(root, password string) []string {
env := make([]string, 0, len(os.Environ())+4)
for _, entry := range os.Environ() {
key, _, _ := strings.Cut(entry, "=")
key = strings.ToUpper(key)
if strings.HasPrefix(key, "MYSQL") || key == "LANG" || key == "LC_ALL" {
continue
}
env = append(env, entry)
}
// --no-defaults 仍可能读取登录文件;指定隔离目录中不存在的文件,避免读取用户凭据。
return append(env, "MYSQL_PWD="+password, "MYSQL_TEST_LOGIN_FILE="+filepath.Join(root, "no-login.cnf"), "LANG=C", "LC_ALL=C")
}