| 1 | package plugin |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "golang.org/x/text/encoding/simplifiedchinese" |
| 11 | ) |
| 12 | |
| 13 | const codePageNotFound = "'read-only-mysql-mcp-server' 不是内部或外部命令,也不是可运行的程序" |
| 14 | |
| 15 | // cmd.exe on a Chinese Windows reports a missing command in the console code |
| 16 | // page, not UTF-8; the failure the settings page shows must still be readable. |
| 17 | func TestStdioFailureDecodesCodePageStderr(t *testing.T) { |
| 18 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| 19 | defer cancel() |
| 20 | |
| 21 | host, _ := StartAvailable(ctx, []Spec{{ |
| 22 | Name: "codepage", |
| 23 | Command: os.Args[0], |
| 24 | Args: []string{"-test.run=TestCodePageStderrHelper", "--"}, |
| 25 | Env: map[string]string{"GO_WANT_HELPER_STDERR_CODEPAGE": "1"}, |
| 26 | }}) |
| 27 | defer host.Close() |
| 28 | |
| 29 | failures := host.Failures() |
| 30 | if len(failures) != 1 { |
| 31 | t.Fatalf("failures = %+v, want one", failures) |
| 32 | } |
| 33 | if !strings.Contains(failures[0].Error, codePageNotFound) { |
| 34 | t.Fatalf("failure should carry the decoded stderr, got %q", failures[0].Error) |
| 35 | } |
| 36 | } |
| 37 | |
| 38 | // TestCodePageStderrHelper is not a real test: as a child it writes a GBK |
| 39 | // line to stderr and exits, the way cmd.exe reports a missing command. |
| 40 | func TestCodePageStderrHelper(t *testing.T) { |
| 41 | if os.Getenv("GO_WANT_HELPER_STDERR_CODEPAGE") != "1" { |
| 42 | return |
| 43 | } |
| 44 | b, _ := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(codePageNotFound)) |
| 45 | os.Stderr.Write(b) |
| 46 | os.Exit(1) |
| 47 | } |
| 48 | |
| 49 | func gbk(t *testing.T, s string) []byte { |
| 50 | t.Helper() |
| 51 | b, err := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(s)) |
| 52 | if err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | return b |
| 56 | } |
| 57 | |
| 58 | // A GBK lead byte is not a UTF-8 rune start, so a tail that begins with a CJK |
| 59 | // character must be decoded whole; only a tail the limit really cut may lose |
| 60 | // part of one. |
| 61 | func TestStderrTailDecodesTheServersCodePage(t *testing.T) { |
| 62 | const line = "参数格式不正确,服务器无法启动" |
| 63 | whole := gbk(t, line) |
| 64 | for _, tc := range []struct { |
| 65 | name string |
| 66 | limit int |
| 67 | write [][]byte |
| 68 | want string |
| 69 | }{ |
| 70 | {"uncut GBK starting with CJK", 16 * 1024, [][]byte{whole}, line}, |
| 71 | {"cut GBK starting with CJK", len(whole), [][]byte{gbk(t, "旧输出"), whole}, line}, |
| 72 | {"UTF-8 cut mid-character", len(line) - 1, [][]byte{[]byte(line)}, line[len("参"):]}, |
| 73 | } { |
| 74 | t.Run(tc.name, func(t *testing.T) { |
| 75 | b := &tailBuffer{limit: tc.limit} |
| 76 | for _, p := range tc.write { |
| 77 | _, _ = b.Write(p) |
| 78 | } |
| 79 | if got := b.String(); got != tc.want { |
| 80 | t.Fatalf("String() = %q, want %q", got, tc.want) |
| 81 | } |
| 82 | }) |
| 83 | } |
| 84 | } |
| 85 |