返回 DeepSeek-Reasonix
decode_test.go
根目录 / internal / shellrun / decode_test.go
1 package shellrun
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "sync"
8 "testing"
9 "time"
10
11 "golang.org/x/text/encoding/simplifiedchinese"
12
13 "reasonix/internal/sandbox"
14 "reasonix/internal/tool"
15 )
16
17 const codePageLine = "FIND: 参数格式不正确\r\n"
18
19 func gbkBytes(t *testing.T, s string) []byte {
20 t.Helper()
21 b, err := simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(s))
22 if err != nil {
23 t.Fatal(err)
24 }
25 return b
26 }
27
28 func octalPrintf(b []byte) string {
29 var sb strings.Builder
30 sb.WriteString("printf '")
31 for _, c := range b {
32 fmt.Fprintf(&sb, "\\%03o", c)
33 }
34 sb.WriteString("'")
35 return sb.String()
36 }
37
38 // A Windows console tool answers in the machine's code page (cp936 on a Chinese
39 // install); left undecoded, JSON turns every byte of it into U+FFFD.
40 func TestRunForegroundDecodesCodePageOutput(t *testing.T) {
41 argv, sh := shellArgv(t, octalPrintf(gbkBytes(t, codePageLine))+"; exit 3")
42 if sh.Kind == sandbox.ShellPowerShell {
43 t.Skip("printf octal escapes need a POSIX shell")
44 }
45 res := RunForeground(context.Background(), Request{Argv: argv, Timeout: 30 * time.Second})
46 if res.State != tool.ShellStateFailed {
47 t.Fatalf("state = %q", res.State)
48 }
49 if res.Combined != codePageLine {
50 t.Fatalf("Combined = %q, want %q", res.Combined, codePageLine)
51 }
52 if res.OutputTail != codePageLine {
53 t.Fatalf("OutputTail = %q, want %q", res.OutputTail, codePageLine)
54 }
55 }
56
57 // A GBK lead byte is not a UTF-8 rune start, so output that begins with a CJK
58 // character must be decoded from its raw bytes; only an edge a buffer really
59 // cut may lose part of a character.
60 func TestBuffersDecodeCodePageOutputStartingWithCJK(t *testing.T) {
61 const text = "参数格式不正确"
62 line := gbkBytes(t, text)
63 t.Run("uncut tail", func(t *testing.T) {
64 c := newOutputCollector(1<<20, 1<<10)
65 _, _ = c.tail.Write(line)
66 if got := c.tailString(); got != text {
67 t.Fatalf("OutputTail = %q, want %q", got, text)
68 }
69 })
70 t.Run("cut tail", func(t *testing.T) {
71 c := newOutputCollector(1<<20, len(line))
72 _, _ = c.tail.Write(gbkBytes(t, "旧输出"))
73 _, _ = c.tail.Write(line)
74 if got := c.tailString(); got != text {
75 t.Fatalf("OutputTail = %q, want %q", got, text)
76 }
77 })
78 t.Run("uncut combined", func(t *testing.T) {
79 c := newOutputCollector(1<<20, 1<<10)
80 _, _ = c.combined.Write(line)
81 if got := c.combined.String(); got != text {
82 t.Fatalf("Combined = %q, want %q", got, text)
83 }
84 })
85 t.Run("cut combined", func(t *testing.T) {
86 b := &boundedBuffer{mu: &sync.Mutex{}, limit: 3*len(line) + 6 + len(line) + 3, tailLimit: len(line), marker: "..."}
87 for range 10 {
88 _, _ = b.Write(line)
89 }
90 want := strings.Repeat(text, 3) + "参数格" + "..." + text
91 if got := b.String(); got != want {
92 t.Fatalf("Combined = %q, want %q", got, want)
93 }
94 })
95 }
96
96 lines GO