Files
MyGoNavi/internal/db/duckdb_i18n_test.go
tianqijiuyun-latiao d13c153f5e feat(i18n): 收口数据库驱动多语言代码
- 提交 internal/db 多驱动用户可见错误与状态文案多语言化

- 补齐数据库驱动多语言测试与六语言 catalog

- 修复 frontend i18n catalog 的 4 个失效 guard
2026-06-22 10:09:45 +08:00

291 lines
7.9 KiB
Go

//go:build gonavi_full_drivers || gonavi_duckdb_driver
package db
import (
"errors"
"database/sql"
"database/sql/driver"
"fmt"
"io"
"os"
"runtime"
"strings"
"sync"
"testing"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/shared/i18n"
)
type duckDBI18nEmptyRowsDriver struct{}
type duckDBI18nEmptyRowsConn struct{}
type duckDBI18nEmptyRowsStmt struct{}
type duckDBI18nEmptyRowsRows struct{}
var registerDuckDBI18nEmptyRowsDriverOnce sync.Once
func (duckDBI18nEmptyRowsDriver) Open(name string) (driver.Conn, error) {
return duckDBI18nEmptyRowsConn{}, nil
}
func (duckDBI18nEmptyRowsConn) Prepare(query string) (driver.Stmt, error) {
return duckDBI18nEmptyRowsStmt{}, nil
}
func (duckDBI18nEmptyRowsConn) Close() error { return nil }
func (duckDBI18nEmptyRowsConn) Begin() (driver.Tx, error) {
return nil, fmt.Errorf("transactions are not supported in duckdb i18n empty rows test driver")
}
func (duckDBI18nEmptyRowsStmt) Close() error { return nil }
func (duckDBI18nEmptyRowsStmt) NumInput() int { return -1 }
func (duckDBI18nEmptyRowsStmt) Exec(args []driver.Value) (driver.Result, error) {
return nil, fmt.Errorf("exec is not supported in duckdb i18n empty rows test driver")
}
func (duckDBI18nEmptyRowsStmt) Query(args []driver.Value) (driver.Rows, error) {
return duckDBI18nEmptyRowsRows{}, nil
}
func (duckDBI18nEmptyRowsRows) Columns() []string {
return []string{"sql"}
}
func (duckDBI18nEmptyRowsRows) Close() error { return nil }
func (duckDBI18nEmptyRowsRows) Next(dest []driver.Value) error {
return io.EOF
}
func openDuckDBI18nEmptyRowsDB(t *testing.T) *sql.DB {
t.Helper()
registerDuckDBI18nEmptyRowsDriverOnce.Do(func() {
sql.Register("duckdb_i18n_empty_rows", duckDBI18nEmptyRowsDriver{})
})
conn, err := sql.Open("duckdb_i18n_empty_rows", "")
if err != nil {
t.Fatalf("open duckdb_i18n_empty_rows test DB failed: %v", err)
}
t.Cleanup(func() {
_ = conn.Close()
})
return conn
}
func TestDuckDBRuntimeUsesCurrentLanguageForConnectionNotOpen(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
err := (&DuckDB{}).Ping()
if err == nil {
t.Fatal("expected Ping to fail when DuckDB connection is not open")
}
if err.Error() != "Connection is not open" {
t.Fatalf("expected English not-open error, got %q", err.Error())
}
}
func TestDuckDBBuildSupportStatusUsesCurrentLanguageWhenDriverIsUnavailable(t *testing.T) {
if supported, _ := duckDBBuildSupportStatus(); supported {
t.Skip("current build already includes DuckDB runtime support")
}
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
wantReason := fmt.Sprintf(
"The current build does not include the DuckDB driver (platform=%s/%s). Enable CGO and use a supported platform (darwin/linux amd64|arm64, windows/amd64), or provide a custom library via -tags duckdb_use_lib / duckdb_use_static_lib",
runtime.GOOS,
runtime.GOARCH,
)
supported, reason := duckDBBuildSupportStatus()
if supported {
t.Fatal("expected DuckDB build support to stay unavailable in this test environment")
}
if reason != wantReason {
t.Fatalf("expected English DuckDB-unavailable reason %q, got %q", wantReason, reason)
}
err := (&DuckDB{}).Connect(connection.ConnectionConfig{Type: "duckdb"})
if err == nil {
t.Fatal("expected DuckDB connect to fail when runtime support is unavailable")
}
wantConnectError := "DuckDB driver is unavailable: " + wantReason
if err.Error() != wantConnectError {
t.Fatalf("expected English DuckDB connect error %q, got %q", wantConnectError, err.Error())
}
}
func TestDuckDBDDLMetadataErrorsUseCurrentLanguage(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
duck := &DuckDB{}
tests := []struct {
name string
call func() error
want string
}{
{
name: "create statement table name required",
call: func() error {
_, err := duck.GetCreateStatement("", " ")
return err
},
want: "Table name is required",
},
{
name: "create statement not found",
call: func() error {
_, err := (&DuckDB{conn: openDuckDBI18nEmptyRowsDB(t)}).GetCreateStatement("main", "orders")
return err
},
want: "The CREATE TABLE statement was not found",
},
{
name: "columns table name required",
call: func() error {
_, err := duck.GetColumns("", " ")
return err
},
want: "Table name is required",
},
{
name: "indexes table name required",
call: func() error {
_, err := duck.GetIndexes("", " ")
return err
},
want: "Table name is required",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.call()
if err == nil {
t.Fatal("expected DuckDB DDL metadata call to fail")
}
if err.Error() != tc.want {
t.Fatalf("expected %q, got %q", tc.want, err.Error())
}
})
}
}
func TestDuckDBApplyChangesErrorsUseCurrentLanguage(t *testing.T) {
SetBackendLanguage(i18n.LanguageEnUS)
t.Cleanup(func() {
SetBackendLanguage(i18n.LanguageZhCN)
})
t.Run("delete failure", func(t *testing.T) {
dbConn, state := openDuckDBRecordingDB(t)
state.failDelete = errors.New("delete blocked")
duckdb := &DuckDB{conn: dbConn}
err := duckdb.ApplyChanges("main.events", connection.ChangeSet{
Deletes: []map[string]interface{}{
{"id": 1},
},
})
if err == nil {
t.Fatal("expected delete failure to bubble up")
}
if err.Error() != "Delete failed: delete blocked" {
t.Fatalf("expected English delete failure, got %q", err.Error())
}
})
t.Run("update key condition required", func(t *testing.T) {
dbConn, _ := openDuckDBRecordingDB(t)
duckdb := &DuckDB{conn: dbConn}
err := duckdb.ApplyChanges("main.events", connection.ChangeSet{
Updates: []connection.UpdateRow{{
Values: map[string]interface{}{
"name": "renamed",
},
}},
})
if err == nil {
t.Fatal("expected update without keys to fail")
}
if err.Error() != "Update operation requires key conditions" {
t.Fatalf("expected English update-key failure, got %q", err.Error())
}
})
t.Run("update failure", func(t *testing.T) {
dbConn, state := openDuckDBRecordingDB(t)
state.failUpdate = errors.New("update blocked")
duckdb := &DuckDB{conn: dbConn}
err := duckdb.ApplyChanges("main.events", connection.ChangeSet{
Updates: []connection.UpdateRow{{
Keys: map[string]interface{}{
"id": 1,
},
Values: map[string]interface{}{
"name": "renamed",
},
}},
})
if err == nil {
t.Fatal("expected update failure to bubble up")
}
if err.Error() != "Update failed: update blocked" {
t.Fatalf("expected English update failure, got %q", err.Error())
}
})
}
func TestDuckDBUserVisibleRuntimeErrorsDoNotReintroduceInlineChinese(t *testing.T) {
t.Helper()
files := []string{"duckdb_impl.go", "duckdb_platform_unsupported.go"}
disallowed := []string{
string([]rune{0x44, 0x75, 0x63, 0x6b, 0x44, 0x42, 0x20, 0x9a71, 0x52a8, 0x4e0d, 0x53ef, 0x7528}),
string([]rune{0x6253, 0x5f00, 0x6570, 0x636e, 0x5e93, 0x8fde, 0x63a5, 0x5931, 0x8d25}),
string([]rune{0x8fde, 0x63a5, 0x5efa, 0x7acb, 0x540e, 0x9a8c, 0x8bc1, 0x5931, 0x8d25}),
string([]rune{0x8fde, 0x63a5, 0x672a, 0x6253, 0x5f00}),
string([]rune{0x5f53, 0x524d, 0x6784, 0x5efa, 0x4e0d, 0x5305, 0x542b, 0x20, 0x44, 0x75, 0x63, 0x6b, 0x44, 0x42, 0x20, 0x9a71, 0x52a8}),
string([]rune{0x8868, 0x540d, 0x4e0d, 0x80fd, 0x4e3a, 0x7a7a}),
string([]rune{0x672a, 0x627e, 0x5230, 0x5efa, 0x8868, 0x8bed, 0x53e5}),
string([]rune{0x5220, 0x9664, 0x5931, 0x8d25}),
string([]rune{0x66f4, 0x65b0, 0x64cd, 0x4f5c, 0x9700, 0x8981, 0x4e3b, 0x952e, 0x6761, 0x4ef6}),
string([]rune{0x66f4, 0x65b0, 0x5931, 0x8d25}),
}
for _, file := range files {
content, err := os.ReadFile(file)
if err != nil {
t.Fatalf("read %s failed: %v", file, err)
}
source := string(content)
for _, raw := range disallowed {
if strings.Contains(source, raw) {
t.Fatalf("%s still contains inline user-visible Chinese raw: %s", file, raw)
}
}
}
}