Files
MyGoNavi/internal/sync/analyze_i18n_test.go
2026-06-22 15:12:42 +08:00

400 lines
12 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 sync
import (
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/db"
"GoNavi-Wails/shared/i18n"
"errors"
"os"
"strings"
"testing"
)
func baseAnalyzeI18nConfig() SyncConfig {
return SyncConfig{
SourceConfig: connection.ConnectionConfig{Type: "mysql", Database: "app"},
TargetConfig: connection.ConnectionConfig{Type: "mysql", Database: "app"},
Tables: []string{"users"},
Mode: "insert_update",
JobID: "analyze-i18n-job",
}
}
func assertNoLegacyAnalyzeChinese(t *testing.T, text string) {
t.Helper()
legacyFragments := []string{
"差异分析开始",
"分析表(",
"差异分析完成",
"已完成 1 张表的差异分析",
"初始化源数据库驱动失败",
"初始化目标数据库驱动失败",
"源数据库连接失败",
"目标数据库连接失败",
"读取源表失败",
"无主键,不支持差异对比同步",
"复合主键(",
}
for _, fragment := range legacyFragments {
if strings.Contains(text, fragment) {
t.Fatalf("expected no legacy Analyze Chinese fragment %q in %q", fragment, text)
}
}
}
func TestAnalyzeUsesLocalizedBackendTextSourceGuard(t *testing.T) {
sourceBytes, err := os.ReadFile("analyze.go")
if err != nil {
t.Fatalf("read analyze.go: %v", err)
}
source := string(sourceBytes)
legacyMessages := []string{
`"差异分析开始"`,
`fmt.Sprintf("分析表(%d/%d)", i+1, totalTables)`,
`"差异分析完成"`,
`fmt.Sprintf("已完成 %d 张表的差异分析", len(result.Tables))`,
`"初始化源数据库驱动失败: " + err.Error()`,
`"初始化目标数据库驱动失败: " + err.Error()`,
`"源数据库连接失败: " + err.Error()`,
`"目标数据库连接失败: " + err.Error()`,
`"目标表不存在,无法执行同步"`,
`fmt.Sprintf("检测到 %d 条结构变更", summary.SchemaDiffCount)`,
`"仅同步结构,未执行数据差异分析"`,
`"读取源表失败: " + err.Error()`,
`"目标表不存在,执行时将自动建表并导入全部源数据"`,
`"当前模式无需差异对比,将按源表数据执行导入"`,
`"无主键,不支持差异对比同步;如需直接导入请使用仅插入或全量覆盖模式"`,
`fmt.Sprintf("复合主键(%s暂不支持差异对比同步", strings.Join(pkCols, ","))`,
`"读取目标表失败: " + err.Error()`,
`"差异分析完成"`,
}
for _, legacy := range legacyMessages {
if strings.Contains(source, legacy) {
t.Fatalf("analyze.go still contains legacy raw user-visible message %q", legacy)
}
}
requiredKeys := []string{
"data_sync.progress.stage.analysis_started",
"data_sync.progress.stage.analysis_completed",
"data_sync.progress.stage.analyzing_table",
"data_sync.backend.error.init_source_driver_failed",
"data_sync.backend.error.init_target_driver_failed",
"data_sync.backend.error.connect_source_failed",
"data_sync.backend.error.connect_target_failed",
"data_sync.backend.error.read_source_table_failed",
"data_sync.backend.error.read_target_table_failed",
"data_sync.backend.error.diff_pk_required",
"data_sync.backend.error.diff_composite_pk_unsupported",
"data_sync.backend.result.analyzed_tables",
"data_sync.backend.summary.diff_completed",
"data_sync.plan.target_missing_cannot_sync",
"data_sync.plan.schema_changes_detected",
"data_sync.plan.schema_only_no_data_diff",
"data_sync.plan.target_missing_auto_create_all",
"data_sync.plan.data_import_without_diff",
}
for _, key := range requiredKeys {
if !strings.Contains(source, key) {
t.Fatalf("analyze.go should reference localized key %q", key)
}
}
}
func TestAnalyzeCatalogKeysExist(t *testing.T) {
catalogs, err := i18n.LoadCatalogs()
if err != nil {
t.Fatalf("LoadCatalogs() error = %v", err)
}
keys := []string{
"data_sync.progress.stage.analysis_started",
"data_sync.progress.stage.analysis_completed",
"data_sync.progress.stage.analyzing_table",
"data_sync.backend.error.init_source_driver_failed",
"data_sync.backend.error.init_target_driver_failed",
"data_sync.backend.error.connect_source_failed",
"data_sync.backend.error.connect_target_failed",
"data_sync.backend.error.read_source_table_failed",
"data_sync.backend.error.read_target_table_failed",
"data_sync.backend.error.diff_pk_required",
"data_sync.backend.error.diff_composite_pk_unsupported",
"data_sync.backend.result.analyzed_tables",
"data_sync.backend.summary.diff_completed",
"data_sync.plan.target_missing_cannot_sync",
"data_sync.plan.schema_changes_detected",
"data_sync.plan.schema_only_no_data_diff",
"data_sync.plan.target_missing_auto_create_all",
"data_sync.plan.data_import_without_diff",
}
for _, language := range i18n.SupportedLanguages() {
catalog := catalogs[language]
for _, key := range keys {
if strings.TrimSpace(catalog[key]) == "" {
t.Fatalf("%s catalog missing Analyze key %q", language, key)
}
}
}
}
func TestAnalyzeUsesCurrentLanguageForInitAndConnectFailures(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
cases := []struct {
name string
steps []syncDatabaseFactoryStep
wantKey string
wantParams map[string]any
}{
{
name: "init source driver failed",
steps: []syncDatabaseFactoryStep{
{err: errors.New("source init boom")},
},
wantKey: "data_sync.backend.error.init_source_driver_failed",
wantParams: map[string]any{
"detail": "source init boom",
},
},
{
name: "init target driver failed",
steps: []syncDatabaseFactoryStep{
{db: &fakeMigrationDB{}},
{err: errors.New("target init boom")},
},
wantKey: "data_sync.backend.error.init_target_driver_failed",
wantParams: map[string]any{
"detail": "target init boom",
},
},
{
name: "connect source failed",
steps: []syncDatabaseFactoryStep{
{db: &connectErrorMigrationDB{connectErr: errors.New("source connect boom")}},
{db: &fakeMigrationDB{}},
},
wantKey: "data_sync.backend.error.connect_source_failed",
wantParams: map[string]any{
"detail": "source connect boom",
},
},
{
name: "connect target failed",
steps: []syncDatabaseFactoryStep{
{db: &fakeMigrationDB{}},
{db: &connectErrorMigrationDB{connectErr: errors.New("target connect boom")}},
},
wantKey: "data_sync.backend.error.connect_target_failed",
wantParams: map[string]any{
"detail": "target connect boom",
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
useSyncDatabaseFactorySequence(t, tc.steps...)
result := NewSyncEngine(Reporter{}).Analyze(baseAnalyzeI18nConfig())
if result.Success {
t.Fatalf("expected Analyze failure for %s, got %+v", tc.name, result)
}
want := localizedSyncTestText(t, i18n.LanguageEnUS, tc.wantKey, tc.wantParams)
if result.Message != want {
t.Fatalf("expected localized Analyze failure message %q, got %q", want, result.Message)
}
assertNoLegacyAnalyzeChinese(t, result.Message)
})
}
}
func TestAnalyzeUsesCurrentLanguageForResultAndProgressStages(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
cols := []connection.ColumnDefinition{
{Name: "id", Type: "bigint", Nullable: "NO", Key: "PRI"},
{Name: "name", Type: "varchar(64)", Nullable: "YES"},
}
sourceDB := &fakeMigrationDB{
columns: map[string][]connection.ColumnDefinition{
"app.users": cols,
},
}
targetDB := &fakeMigrationDB{
columns: map[string][]connection.ColumnDefinition{
"app.users": cols,
},
}
useSyncDatabaseFactorySequence(t,
syncDatabaseFactoryStep{db: sourceDB},
syncDatabaseFactoryStep{db: targetDB},
)
var stages []string
engine := NewSyncEngine(Reporter{
OnProgress: func(event SyncProgressEvent) {
stages = append(stages, event.Stage)
},
})
config := baseAnalyzeI18nConfig()
config.Content = "schema"
result := engine.Analyze(config)
if !result.Success {
t.Fatalf("expected Analyze success, got %+v", result)
}
wantResult := localizedSyncTestText(t, i18n.LanguageEnUS, "data_sync.backend.result.analyzed_tables", map[string]any{
"count": 1,
})
if result.Message != wantResult {
t.Fatalf("expected localized Analyze result message %q, got %q", wantResult, result.Message)
}
assertNoLegacyAnalyzeChinese(t, result.Message)
wantStages := []string{
localizedSyncTestText(t, i18n.LanguageEnUS, "data_sync.progress.stage.analysis_started", nil),
localizedSyncTestText(t, i18n.LanguageEnUS, "data_sync.progress.stage.analyzing_table", map[string]any{
"current": 1,
"total": 1,
}),
localizedSyncTestText(t, i18n.LanguageEnUS, "data_sync.progress.stage.analysis_completed", nil),
}
if len(stages) != len(wantStages) {
t.Fatalf("unexpected progress stage count: got=%v want=%v", stages, wantStages)
}
for idx, wantStage := range wantStages {
if stages[idx] != wantStage {
t.Fatalf("stage[%d] = %q, want %q; all stages=%v", idx, stages[idx], wantStage, stages)
}
assertNoLegacyAnalyzeChinese(t, stages[idx])
}
}
func TestAnalyzeUsesCurrentLanguageForReadAndPKMessages(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
countQuery := "SELECT COUNT(*) AS __gonavi_count__ FROM `app`.`users`"
noPKCols := []connection.ColumnDefinition{
{Name: "id", Type: "bigint", Nullable: "NO"},
{Name: "name", Type: "varchar(64)", Nullable: "YES"},
}
compositePKCols := []connection.ColumnDefinition{
{Name: "id", Type: "bigint", Nullable: "NO", Key: "PRI"},
{Name: "tenant_id", Type: "bigint", Nullable: "NO", Key: "PRI"},
{Name: "name", Type: "varchar(64)", Nullable: "YES"},
}
singlePKCols := []connection.ColumnDefinition{
{Name: "id", Type: "bigint", Nullable: "NO", Key: "PRI"},
{Name: "name", Type: "varchar(64)", Nullable: "YES"},
}
cases := []struct {
name string
sourceDB db.Database
targetDB db.Database
wantKey string
wantParams map[string]any
}{
{
name: "read source table failed",
sourceDB: &errorMigrationDB{
fakeMigrationDB: fakeMigrationDB{
columns: map[string][]connection.ColumnDefinition{
"app.users": singlePKCols,
},
},
queryErrors: map[string]error{
countQuery: errors.New("source count boom"),
},
},
targetDB: &fakeMigrationDB{
columns: map[string][]connection.ColumnDefinition{
"app.users": singlePKCols,
},
},
wantKey: "data_sync.backend.error.read_source_table_failed",
wantParams: map[string]any{
"detail": "source count boom",
},
},
{
name: "no primary key",
sourceDB: &fakeMigrationDB{
columns: map[string][]connection.ColumnDefinition{
"app.users": noPKCols,
},
queryData: map[string][]map[string]interface{}{
countQuery: {
{"__gonavi_count__": 2},
},
},
},
targetDB: &fakeMigrationDB{
columns: map[string][]connection.ColumnDefinition{
"app.users": noPKCols,
},
},
wantKey: "data_sync.backend.error.diff_pk_required",
},
{
name: "composite primary key unsupported",
sourceDB: &fakeMigrationDB{
columns: map[string][]connection.ColumnDefinition{
"app.users": compositePKCols,
},
queryData: map[string][]map[string]interface{}{
countQuery: {
{"__gonavi_count__": 2},
},
},
},
targetDB: &fakeMigrationDB{
columns: map[string][]connection.ColumnDefinition{
"app.users": compositePKCols,
},
},
wantKey: "data_sync.backend.error.diff_composite_pk_unsupported",
wantParams: map[string]any{
"columns": "id,tenant_id",
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
useSyncDatabaseFactorySequence(t,
syncDatabaseFactoryStep{db: tc.sourceDB},
syncDatabaseFactoryStep{db: tc.targetDB},
)
result := NewSyncEngine(Reporter{}).Analyze(baseAnalyzeI18nConfig())
if !result.Success {
t.Fatalf("expected Analyze summary result for %s, got %+v", tc.name, result)
}
if len(result.Tables) != 1 {
t.Fatalf("expected one table summary, got %d", len(result.Tables))
}
want := localizedSyncTestText(t, i18n.LanguageEnUS, tc.wantKey, tc.wantParams)
if result.Tables[0].Message != want {
t.Fatalf("expected localized Analyze table message %q, got %q", want, result.Tables[0].Message)
}
assertNoLegacyAnalyzeChinese(t, result.Tables[0].Message)
})
}
}