| 1 | package persistentshell |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "path/filepath" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | // Desktop launches can omit all locale variables. The command must reach |
| 13 | // interactive Bash unchanged even when Readline treats high bytes as meta keys. |
| 14 | func TestPersistentShellUnicodeTransport(t *testing.T) { |
| 15 | skipNonPOSIX(t) |
| 16 | sh := posixShell(t) |
| 17 | for _, locale := range []string{"unset", "C", "en_US.UTF-8"} { |
| 18 | t.Run(locale, func(t *testing.T) { |
| 19 | dir := t.TempDir() |
| 20 | m := testManager(t) |
| 21 | env := []string{"PATH=" + os.Getenv("PATH"), "HOME=" + dir, "TERM=dumb", "INPUTRC=/dev/null"} |
| 22 | if locale != "unset" { |
| 23 | env = append(env, "LC_ALL="+locale) |
| 24 | } |
| 25 | req := Request{Argv: InteractiveArgv(sh), Dir: dir, Env: env, Shell: sh, Timeout: 10 * time.Second} |
| 26 | run := func(command, want string) { |
| 27 | t.Helper() |
| 28 | req.Command = command |
| 29 | res := m.Run(context.Background(), req) |
| 30 | if res.Err != nil || res.ExitCode != 0 || res.Output != want { |
| 31 | t.Fatalf("command (%d bytes): err=%v exit=%d output=%q, want %q", len(command), res.Err, res.ExitCode, res.Output, want) |
| 32 | } |
| 33 | } |
| 34 | run("printf '%s' '起点是空白格😀7'", "起点是空白格😀7") |
| 35 | // Exercise the incident's grep shape using a disposable Unicode path. |
| 36 | if err := os.WriteFile(filepath.Join(dir, "中文 '文件.txt"), []byte("起点是空白格\nother\n"), 0o600); err != nil { |
| 37 | t.Fatal(err) |
| 38 | } |
| 39 | run("grep -n '起点是空白格' "+posixQuote("中文 '文件.txt"), "1:起点是空白格\n") |
| 40 | text := "中文 'quote' \\ $(false) 😀" |
| 41 | run("value="+posixQuote(text)+"; printf '%s' \"$value\"", text) |
| 42 | run("printf '%s' \"$value\"", text) |
| 43 | run("cat <<'EOF'\n中文😀\nEOF", "中文😀\n") |
| 44 | // The ASCII encoding expands non-ASCII input fourfold. Exercise a |
| 45 | // wrapper well beyond a canonical terminal line buffer and read chunk. |
| 46 | long := strings.Repeat("中文😀", 2000) |
| 47 | run("printf '%s' "+posixQuote(long), long) |
| 48 | // Locale is user command state, not something transport may replace. |
| 49 | run("export LC_ALL=C", "") |
| 50 | run("printf '%s' \"$LC_ALL\"; printf '%s' '中文'", "C中文") |
| 51 | |
| 52 | ctx, cancel := context.WithCancel(context.Background()) |
| 53 | defer cancel() |
| 54 | // Exceed the capture's marker overlap so progress is delivered |
| 55 | // while the command is running, not only when partial output flushes. |
| 56 | req.Command = "printf 'started\\n%0128d\\n' 0; sleep 30" |
| 57 | req.Progress = &cancelOnStarted{cancel: cancel} |
| 58 | res := m.Run(ctx, req) |
| 59 | if !res.Canceled || !res.Reset { |
| 60 | t.Fatalf("cancel must retire the running shell: %+v", res) |
| 61 | } |
| 62 | req.Progress = nil |
| 63 | run("printf '%s' '恢复😀'", "恢复😀") |
| 64 | }) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | type cancelOnStarted struct { |
| 69 | output strings.Builder |
| 70 | cancel context.CancelFunc |
| 71 | } |
| 72 | |
| 73 | // Between prompts the PTY can be in canonical mode. Disabling line editing |
| 74 | // keeps that state deterministic and must not truncate an encoded command. |
| 75 | func TestPersistentShellLongCommandWithoutLineEditing(t *testing.T) { |
| 76 | skipNonPOSIX(t) |
| 77 | sh := posixShell(t) |
| 78 | dir := t.TempDir() |
| 79 | m := testManager(t) |
| 80 | argv := append([]string{sh.Path, "--noediting"}, InteractiveArgv(sh)[1:]...) |
| 81 | req := Request{Argv: argv, Dir: dir, Shell: sh, Timeout: 10 * time.Second, |
| 82 | Env: []string{"PATH=" + os.Getenv("PATH"), "HOME=" + dir, "TERM=dumb", "INPUTRC=/dev/null", "LC_ALL=C"}, |
| 83 | Command: "PS2=CONTINUATION_PROMPT"} |
| 84 | if res := m.Run(t.Context(), req); res.Err != nil { |
| 85 | t.Fatal(res.Err) |
| 86 | } |
| 87 | text := strings.Repeat("中文😀", 2000) |
| 88 | req.Command = "value=" + posixQuote(text) + "; printf '%s' \"$value\"" |
| 89 | res := m.Run(t.Context(), req) |
| 90 | if res.Err != nil || res.Output != text { |
| 91 | t.Fatalf("long command: err=%v output bytes=%d want=%d", res.Err, len(res.Output), len(text)) |
| 92 | } |
| 93 | req.Command = "printf '%s' \"$value\"" |
| 94 | if res := m.Run(t.Context(), req); res.Err != nil || res.Output != text { |
| 95 | t.Fatalf("retained value: err=%v output bytes=%d want=%d", res.Err, len(res.Output), len(text)) |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | func (w *cancelOnStarted) Write(p []byte) (int, error) { |
| 100 | w.output.Write(p) |
| 101 | if strings.Contains(w.output.String(), "started\n") { |
| 102 | w.cancel() |
| 103 | } |
| 104 | return len(p), nil |
| 105 | } |
| 106 |