返回 DeepSeek-Reasonix
utf8_test.go
根目录 / internal / shellrun / utf8_test.go
1 package shellrun
2
3 import (
4 "context"
5 "encoding/json"
6 "os/exec"
7 "reasonix/internal/proc"
8 "strings"
9 "sync"
10 "testing"
11 "unicode/utf8"
12 )
13
14 func TestForegroundFlushesUTF8OnCompletionAndCancel(t *testing.T) {
15 for _, canceled := range []bool{false, true} {
16 var chunks []string
17 ctx, cancel := context.WithCancel(context.Background())
18 RunForeground(ctx, Request{Argv: []string{"fixture"}, Progress: jsonProgressCollector(t, &chunks),
19 Run: func(_ context.Context, cmd *exec.Cmd, _ proc.RunOptions) (*proc.TrackedCommand, error) {
20 _, _ = cmd.Stdout.Write([]byte{'A', 0xe4, 0xb8})
21 if canceled {
22 cancel()
23 return nil, context.Canceled
24 }
25 return nil, nil
26 },
27 })
28 cancel()
29 if got := strings.Join(chunks, ""); got != "A\ufffd" {
30 t.Fatalf("cancel=%v got %q", canceled, got)
31 }
32 }
33 }
34
35 func jsonProgressCollector(t *testing.T, chunks *[]string) func(string) {
36 t.Helper()
37 return func(s string) {
38 t.Helper()
39 if !utf8.ValidString(s) {
40 t.Errorf("progress event contains incomplete UTF-8: %x", []byte(s))
41 }
42 encoded, err := json.Marshal(s)
43 if err != nil {
44 t.Fatal(err)
45 }
46 var decoded string
47 if err := json.Unmarshal(encoded, &decoded); err != nil {
48 t.Fatal(err)
49 }
50 *chunks = append(*chunks, decoded)
51 }
52 }
53
54 func TestProgressWriterUTF8SplitJSON(t *testing.T) {
55 const source = "中文路径/😀.txt\n"
56 for split := 1; split < len(source); split++ {
57 var chunks []string
58 w := newProgressWriter(jsonProgressCollector(t, &chunks), 1024, "[truncated]")
59 for _, p := range []string{source[:split], source[split:]} {
60 if n, err := w.Write([]byte(p)); err != nil || n != len(p) {
61 t.Fatalf("split=%d Write=%d,%v", split, n, err)
62 }
63 }
64 if got := strings.Join(chunks, ""); got != source {
65 t.Errorf("split=%d JSON roundtrip=%q, want %q", split, got, source)
66 }
67 }
68 }
69
70 func TestProgressWriterUTF8ByteCap(t *testing.T) {
71 const source = "中😀文"
72 for limit := 1; limit <= len(source); limit++ {
73 var chunks []string
74 w := newProgressWriter(jsonProgressCollector(t, &chunks), limit, "[truncated]")
75 for i := range len(source) {
76 _, _ = w.Write([]byte(source[i : i+1]))
77 }
78 end := limit
79 for !utf8.ValidString(source[:end]) {
80 end--
81 }
82 want := source[:end]
83 if limit < len(source) {
84 want += "[truncated]"
85 }
86 if got := strings.Join(chunks, ""); got != want {
87 t.Errorf("limit=%d got %q, want %q", limit, got, want)
88 }
89 }
90 }
91
92 // The owner flushes when a stream ends, including early termination. A partial
93 // character cannot be recovered then, but must not disappear or corrupt JSON.
94 func TestProgressWriterUTF8FlushPartial(t *testing.T) {
95 var chunks []string
96 w := newProgressWriter(jsonProgressCollector(t, &chunks), 1024, "[truncated]")
97 _, _ = w.Write([]byte{'A', 0xe4, 0xb8})
98 if got := strings.Join(chunks, ""); got != "A" {
99 t.Errorf("incomplete character emitted before flush: %q", got)
100 }
101 flusher, ok := any(w).(interface{ Flush() })
102 if !ok {
103 t.Fatal("progressWriter must expose Flush for stream EOF and cancellation")
104 }
105 flusher.Flush()
106 flusher.Flush()
107 if got := strings.Join(chunks, ""); got != "A\ufffd" {
108 t.Errorf("idempotent final flush=%q, want one replacement for unfinished rune", got)
109 }
110 }
111
112 func TestBoundedBufferUTF8HeadTail(t *testing.T) {
113 const source = "头😀中间内容尾😀"
114 for split := 1; split < len(source); split++ {
115 b := &boundedBuffer{mu: &sync.Mutex{}, limit: 16, tailLimit: 5, marker: "..."}
116 _, _ = b.Write([]byte(source[:split]))
117 _, _ = b.Write([]byte(source[split:]))
118 got := b.String()
119 if !utf8.ValidString(got) || len(got) > b.limit {
120 t.Errorf("split=%d invalid or over cap: %q (%x)", split, got, []byte(got))
121 }
122 if got != "头😀...😀" {
123 t.Errorf("split=%d got %q, want complete head/tail runes", split, got)
124 }
125 }
126 }
127
127 lines GO