| 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 |