Skip to content

Commit e50334f

Browse files
committed
test message
1 parent 71ca413 commit e50334f

2 files changed

Lines changed: 187 additions & 51 deletions

File tree

cmd/interactive.go

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package cmd
44
import (
55
"bufio"
66
"fmt"
7+
"io"
78
"math"
89
"os"
910
"strings"
@@ -17,6 +18,40 @@ type CommandInfo struct {
1718
Description string
1819
}
1920

21+
// UI represents the interface for terminal UI operations
22+
type UI struct {
23+
stdin io.Reader
24+
stdout io.Writer
25+
stderr io.Writer
26+
term terminal
27+
}
28+
29+
// terminal represents terminal operations
30+
type terminal interface {
31+
makeRaw(fd int) (*term.State, error)
32+
restore(fd int, state *term.State) error
33+
}
34+
35+
type defaultTerminal struct{}
36+
37+
func (t *defaultTerminal) makeRaw(fd int) (*term.State, error) {
38+
return term.MakeRaw(fd)
39+
}
40+
41+
func (t *defaultTerminal) restore(fd int, state *term.State) error {
42+
return term.Restore(fd, state)
43+
}
44+
45+
// NewUI creates a new UI with default settings
46+
func NewUI() *UI {
47+
return &UI{
48+
stdin: os.Stdin,
49+
stdout: os.Stdout,
50+
stderr: os.Stderr,
51+
term: &defaultTerminal{},
52+
}
53+
}
54+
2055
var commands = []CommandInfo{
2156
{"add <file>", "Add a specific file to the index"},
2257
{"add .", "Add all changes to index"},
@@ -74,28 +109,34 @@ var commands = []CommandInfo{
74109
// InteractiveUI provides an incremental search interactive UI for command selection.
75110
// Returns the selected command as []string (nil if nothing selected)
76111
func InteractiveUI() []string {
112+
ui := NewUI()
113+
return ui.Run()
114+
}
115+
116+
// Run executes the interactive UI
117+
func (ui *UI) Run() []string {
77118
fd := int(os.Stdin.Fd())
78-
oldState, err := term.MakeRaw(fd)
119+
oldState, err := ui.term.makeRaw(fd)
79120
if err != nil {
80-
fmt.Println("Failed to set terminal to raw mode:", err)
121+
fmt.Fprintln(ui.stderr, "Failed to set terminal to raw mode:", err)
81122
return nil
82123
}
83124
defer func() {
84-
if err := term.Restore(fd, oldState); err != nil {
85-
fmt.Fprintln(os.Stderr, "failed to restore terminal state:", err)
125+
if err := ui.term.restore(fd, oldState); err != nil {
126+
fmt.Fprintln(ui.stderr, "failed to restore terminal state:", err)
86127
}
87128
}()
88129

89-
reader := bufio.NewReader(os.Stdin)
130+
reader := bufio.NewReader(ui.stdin)
90131
selected := 0
91132
input := ""
92133

93134
for {
94-
if _, err := os.Stdout.Write([]byte("\033[H\033[2J\033[H")); err != nil {
95-
fmt.Fprintln(os.Stderr, "failed to write clear screen sequence:", err)
135+
if _, err := fmt.Fprint(ui.stdout, "\033[H\033[2J\033[H"); err != nil {
136+
fmt.Fprintln(ui.stderr, "failed to write clear screen sequence:", err)
96137
}
97-
fmt.Printf("Select a command (incremental search: type to filter, ctrl+n: down, ctrl+p: up, enter: execute, ctrl+c: quit)\n")
98-
fmt.Printf("\rSearch: %s\n\n", input)
138+
fmt.Fprintf(ui.stdout, "Select a command (incremental search: type to filter, ctrl+n: down, ctrl+p: up, enter: execute, ctrl+c: quit)\n")
139+
fmt.Fprintf(ui.stdout, "\rSearch: %s\n\n", input)
99140

100141
// Filtering
101142
filtered := []CommandInfo{}
@@ -105,10 +146,10 @@ func InteractiveUI() []string {
105146
}
106147
}
107148
if input == "" {
108-
fmt.Println("(Type to filter commands...)")
149+
fmt.Fprintln(ui.stdout, "(Type to filter commands...)")
109150
} else {
110151
if len(filtered) == 0 {
111-
fmt.Println(" (No matching command)")
152+
fmt.Fprintln(ui.stdout, " (No matching command)")
112153
}
113154
if selected >= len(filtered) {
114155
selected = len(filtered) - 1
@@ -131,41 +172,41 @@ func InteractiveUI() []string {
131172
paddingLen := int(math.Max(0, float64(maxCmdLen-len(cmd.Command))))
132173
padding := strings.Repeat(" ", paddingLen)
133174
if i == selected {
134-
fmt.Printf("\r> %s%s %s\n", cmd.Command, padding, desc)
175+
fmt.Fprintf(ui.stdout, "\r> %s%s %s\n", cmd.Command, padding, desc)
135176
} else {
136-
fmt.Printf("\r %s%s %s\n", cmd.Command, padding, desc)
177+
fmt.Fprintf(ui.stdout, "\r %s%s %s\n", cmd.Command, padding, desc)
137178
}
138179
}
139180
}
140-
fmt.Print("\n\r") // Ensure next output starts at left edge
181+
fmt.Fprint(ui.stdout, "\n\r") // Ensure next output starts at left edge
141182

142183
b, err := reader.ReadByte()
143184
if err != nil {
144185
continue
145186
}
146187
if b == 3 { // Ctrl+C in raw mode
147-
if err := term.Restore(fd, oldState); err != nil {
148-
fmt.Fprintln(os.Stderr, "failed to restore terminal state:", err)
188+
if err := ui.term.restore(fd, oldState); err != nil {
189+
fmt.Fprintln(ui.stderr, "failed to restore terminal state:", err)
149190
}
150-
fmt.Println("\nExiting...")
191+
fmt.Fprintln(ui.stdout, "\nExiting...")
151192
os.Exit(0)
152193
} else if b == 13 { // Enter
153194
if input == "" {
154195
continue
155196
}
156197
if len(filtered) > 0 {
157-
fmt.Printf("\nExecute: %s\n", filtered[selected].Command)
158-
if err := term.Restore(fd, oldState); err != nil {
159-
fmt.Fprintln(os.Stderr, "failed to restore terminal state:", err)
198+
fmt.Fprintf(ui.stdout, "\nExecute: %s\n", filtered[selected].Command)
199+
if err := ui.term.restore(fd, oldState); err != nil {
200+
fmt.Fprintln(ui.stderr, "failed to restore terminal state:", err)
160201
}
161202
// Placeholder detection
162203
cmdTemplate := filtered[selected].Command
163204
placeholders := extractPlaceholders(cmdTemplate)
164205
inputs := make(map[string]string)
165-
readerStdin := bufio.NewReader(os.Stdin)
206+
readerStdin := bufio.NewReader(ui.stdin)
166207
for _, ph := range placeholders {
167-
fmt.Print("\n\r") // Newline + carriage return
168-
fmt.Printf("Enter value for %s: ", ph)
208+
fmt.Fprint(ui.stdout, "\n\r") // Newline + carriage return
209+
fmt.Fprintf(ui.stdout, "Enter value for %s: ", ph)
169210
val, _ := readerStdin.ReadString('\n')
170211
val = strings.TrimSpace(val)
171212
inputs[ph] = val

cmd/interactive_test.go

Lines changed: 123 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,152 @@
11
package cmd
22

33
import (
4-
"reflect"
4+
"bytes"
5+
"fmt"
56
"testing"
7+
8+
"golang.org/x/term"
69
)
710

8-
func TestExtractPlaceholders(t *testing.T) {
11+
// mockTerminal はターミナル操作をモック化
12+
type mockTerminal struct {
13+
makeRawCalled bool
14+
restoreCalled bool
15+
shouldFailRaw bool
16+
shouldFailRest bool
17+
}
18+
19+
func (m *mockTerminal) makeRaw(fd int) (*term.State, error) {
20+
m.makeRawCalled = true
21+
if m.shouldFailRaw {
22+
return nil, fmt.Errorf("mock makeRaw error")
23+
}
24+
return &term.State{}, nil
25+
}
26+
27+
func (m *mockTerminal) restore(fd int, state *term.State) error {
28+
m.restoreCalled = true
29+
if m.shouldFailRest {
30+
return fmt.Errorf("mock restore error")
31+
}
32+
return nil
33+
}
34+
35+
// testUI はテスト用のUI構造体
36+
type testUI struct {
37+
UI
38+
inputBytes []byte
39+
}
40+
41+
func (ui *testUI) Run() []string {
42+
// 標準入力をシミュレート
43+
ui.stdin = bytes.NewReader(ui.inputBytes)
44+
ui.stdout = &bytes.Buffer{}
45+
ui.stderr = &bytes.Buffer{}
46+
return ui.UI.Run()
47+
}
48+
49+
func TestUI_Run(t *testing.T) {
950
tests := []struct {
10-
name string
11-
input string
12-
want []string
51+
name string
52+
input []byte
53+
expectedArgs []string
54+
expectNil bool
1355
}{
1456
{
15-
name: "no placeholders",
16-
input: "simple command",
17-
want: []string{},
57+
name: "空入力でEnterを押した場合",
58+
input: []byte{13}, // Enter key
59+
expectNil: true,
1860
},
1961
{
20-
name: "single placeholder",
21-
input: "add <file>",
22-
want: []string{"file"},
62+
name: "helpと入力してEnterを押した場合",
63+
input: []byte("help\r"),
64+
expectedArgs: []string{"ggc", "help"},
65+
expectNil: false,
2366
},
2467
{
25-
name: "multiple placeholders",
26-
input: "remote add <name> <url>",
27-
want: []string{"name", "url"},
68+
name: "存在しないコマンドを入力した場合",
69+
input: []byte("nonexistent\r"),
70+
expectNil: true,
71+
},
72+
}
73+
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
// テスト用のUI作成
77+
ui := &testUI{
78+
UI: UI{
79+
term: &mockTerminal{},
80+
},
81+
inputBytes: tt.input,
82+
}
83+
84+
// テスト実行
85+
result := ui.Run()
86+
87+
// 結果の検証
88+
if tt.expectNil && result != nil {
89+
t.Errorf("期待値: nil, 実際の値: %v", result)
90+
}
91+
92+
if !tt.expectNil {
93+
if result == nil {
94+
t.Error("期待値: not nil, 実際の値: nil")
95+
return
96+
}
97+
if len(result) != len(tt.expectedArgs) {
98+
t.Errorf("期待値の長さ: %d, 実際の値の長さ: %d", len(tt.expectedArgs), len(result))
99+
return
100+
}
101+
for i, arg := range tt.expectedArgs {
102+
if result[i] != arg {
103+
t.Errorf("期待値[%d]: %s, 実際の値[%d]: %s", i, arg, i, result[i])
104+
}
105+
}
106+
}
107+
})
108+
}
109+
}
110+
111+
func TestExtractPlaceholders(t *testing.T) {
112+
tests := []struct {
113+
name string
114+
input string
115+
expected []string
116+
}{
117+
{
118+
name: "プレースホルダーなし",
119+
input: "command",
120+
expected: []string{},
28121
},
29122
{
30-
name: "empty placeholder",
31-
input: "command <>",
32-
want: []string{""},
123+
name: "1つのプレースホルダー",
124+
input: "command <file>",
125+
expected: []string{"file"},
33126
},
34127
{
35-
name: "invalid format",
36-
input: "command <incomplete",
37-
want: []string{},
128+
name: "複数のプレースホルダー",
129+
input: "command <file> <message>",
130+
expected: []string{"file", "message"},
38131
},
39132
{
40-
name: "nested placeholder",
41-
input: "command <<nested>>",
42-
want: []string{"nested"},
133+
name: "不完全なプレースホルダー",
134+
input: "command <file message>",
135+
expected: []string{"file message"},
43136
},
44137
}
45138

46139
for _, tt := range tests {
47140
t.Run(tt.name, func(t *testing.T) {
48-
got := extractPlaceholders(tt.input)
49-
// Compare without distinguishing between nil and empty slice
50-
if len(tt.want) == 0 && len(got) == 0 {
141+
result := extractPlaceholders(tt.input)
142+
if len(result) != len(tt.expected) {
143+
t.Errorf("期待値の長さ: %d, 実際の値の長さ: %d", len(tt.expected), len(result))
51144
return
52145
}
53-
if !reflect.DeepEqual(got, tt.want) {
54-
t.Errorf("extractPlaceholders() = %v, want %v", got, tt.want)
146+
for i, exp := range tt.expected {
147+
if result[i] != exp {
148+
t.Errorf("期待値[%d]: %s, 実際の値[%d]: %s", i, exp, i, result[i])
149+
}
55150
}
56151
})
57152
}

0 commit comments

Comments
 (0)