-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmain_unit_test.go
More file actions
294 lines (263 loc) · 9.83 KB
/
main_unit_test.go
File metadata and controls
294 lines (263 loc) · 9.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
package main
import (
"os"
"strings"
"testing"
"time"
)
func TestEnsureGitExcludeEntryAddsEntry(t *testing.T) {
repoPath := initRepo(t)
if err := ensureGitExcludeEntry(repoPath, defaultWorktreeDir); err != nil {
t.Fatalf("ensure git exclude entry: %v", err)
}
if err := ensureGitExcludeEntry(repoPath, defaultWorktreeDir); err != nil {
t.Fatalf("ensure git exclude entry again: %v", err)
}
excludePath, err := gitExcludePath(repoPath)
if err != nil {
t.Fatalf("resolve git excludes path: %v", err)
}
content, err := os.ReadFile(excludePath)
if err != nil {
t.Fatalf("read git excludes: %v", err)
}
entry := defaultWorktreeDir + "/"
if !strings.Contains(string(content), entry) {
t.Fatalf("expected %q in git excludes", entry)
}
if strings.Count(string(content), entry) != 1 {
t.Fatalf("expected single %q entry in git excludes", entry)
}
}
func TestValidateBranchName(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
// Valid names
{"simple name", "feature", false},
{"with slash", "feature/add-login", false},
{"with numbers", "feature-123", false},
{"with dash", "fix-bug", false},
{"nested slashes", "user/feature/sub", false},
{"underscore", "feature_test", false},
// Invalid: empty or whitespace
{"empty", "", true},
{"only spaces", " ", true},
// Invalid: starts with special chars
{"starts with dash", "-feature", true},
{"starts with dot", ".feature", true},
// Invalid: ends with special chars
{"ends with dot", "feature.", true},
{"ends with lock", "feature.lock", true},
// Invalid: contains ..
{"contains double dot", "feature..test", true},
// Invalid: contains space
{"contains space", "feature test", true},
// Invalid: special git chars
{"contains tilde", "feature~1", true},
{"contains caret", "feature^2", true},
{"contains colon", "feature:test", true},
{"contains question", "feature?", true},
{"contains asterisk", "feature*", true},
{"contains bracket", "feature[1]", true},
{"contains backslash", "feature\\test", true},
// Invalid: shell metacharacters
{"contains semicolon", "feature;rm -rf", true},
{"contains ampersand", "feature&test", true},
{"contains pipe", "feature|test", true},
{"contains dollar", "feature$var", true},
{"contains backtick", "feature`cmd`", true},
{"contains paren open", "feature(test)", true},
{"contains paren close", "feature)", true},
{"contains less than", "feature<test", true},
{"contains greater than", "feature>test", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateBranchName(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("validateBranchName(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
})
}
}
func TestTruncateString(t *testing.T) {
tests := []struct {
name string
input string
maxLen int
want string
}{
{"empty string", "", 10, ""},
{"short string unchanged", "hello", 10, "hello"},
{"exact length unchanged", "hello", 5, "hello"},
{"truncated with ellipsis", "hello world", 5, "hello..."},
{"zero maxLen", "hello", 0, "..."},
{"unicode characters", "日本語テスト", 3, "日本語..."},
{"unicode unchanged", "日本語", 5, "日本語"},
{"mixed unicode ascii", "hello日本語", 7, "hello日本..."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := truncateString(tt.input, tt.maxLen)
if got != tt.want {
t.Errorf("truncateString(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
}
})
}
}
func TestFormatRelativeTime(t *testing.T) {
now := time.Now()
tests := []struct {
name string
time time.Time
want string
}{
{"zero time", time.Time{}, ""},
{"just now", now.Add(-30 * time.Second), "just now"},
{"1 minute ago", now.Add(-1 * time.Minute), "1 minute ago"},
{"5 minutes ago", now.Add(-5 * time.Minute), "5 minutes ago"},
{"1 hour ago", now.Add(-1 * time.Hour), "1 hour ago"},
{"3 hours ago", now.Add(-3 * time.Hour), "3 hours ago"},
{"1 day ago", now.Add(-24 * time.Hour), "1 day ago"},
{"3 days ago", now.Add(-3 * 24 * time.Hour), "3 days ago"},
{"old date formatted", now.Add(-30 * 24 * time.Hour), now.Add(-30 * 24 * time.Hour).Format("Jan 2, 2006")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatRelativeTime(tt.time)
if got != tt.want {
t.Errorf("formatRelativeTime() = %q, want %q", got, tt.want)
}
})
}
}
func TestFilterWorktrees(t *testing.T) {
worktrees := []Worktree{
{Branch: "main", Path: "/repo/main", LastCommit: CommitInfo{Message: "initial commit"}},
{Branch: "feature-auth", Path: "/repo/.worktrees/feature-auth", LastCommit: CommitInfo{Message: "add login"}},
{Branch: "fix-bug-123", Path: "/repo/.worktrees/fix-bug-123", LastCommit: CommitInfo{Message: "fix crash"}},
{Branch: "develop", Path: "/repo/.worktrees/develop", LastCommit: CommitInfo{Message: "merged changes"}},
}
tests := []struct {
name string
search string
want int
}{
{"empty search returns all", "", 4},
{"match by branch", "feature-auth", 1},
{"match by path", "worktrees", 3},
{"match by commit message", "crash", 1},
{"case insensitive branch", "MAIN", 1},
{"case insensitive message", "LOGIN", 1},
{"no matches", "nonexistent", 0},
{"partial match", "auth", 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := filterWorktrees(worktrees, tt.search)
if len(got) != tt.want {
t.Errorf("filterWorktrees(%q) returned %d worktrees, want %d", tt.search, len(got), tt.want)
}
})
}
}
func TestGetShell(t *testing.T) {
tests := []struct {
name string
config *Config
envShell string
want string
setEnv bool
clearEnv bool
}{
{"config shell preferred", &Config{Shell: "/bin/zsh"}, "/bin/bash", "/bin/zsh", true, false},
{"env fallback when no config shell", &Config{}, "/bin/fish", "/bin/fish", true, false},
{"nil config uses env", nil, "/bin/ksh", "/bin/ksh", true, false},
{"default bash when no env", &Config{}, "", "/bin/bash", false, true},
{"empty config shell uses env", &Config{Shell: ""}, "/usr/bin/zsh", "/usr/bin/zsh", true, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.clearEnv {
t.Setenv("SHELL", "")
} else if tt.setEnv {
t.Setenv("SHELL", tt.envShell)
}
got := getShell(tt.config)
if got != tt.want {
t.Errorf("getShell() = %q, want %q", got, tt.want)
}
})
}
}
func TestParseArgs(t *testing.T) {
tests := []struct {
name string
args []string
wantWorktree string
wantSource string
wantExecute string
wantCompletion string
wantHelp bool
wantVersion bool
wantMerge bool
wantMergeStrat string
}{
{"no args", []string{"gt"}, "", "", "", "", false, false, false, ""},
{"help short", []string{"gt", "-h"}, "", "", "", "", true, false, false, ""},
{"help long", []string{"gt", "--help"}, "", "", "", "", true, false, false, ""},
{"help word", []string{"gt", "help"}, "", "", "", "", true, false, false, ""},
{"version short", []string{"gt", "-v"}, "", "", "", "", false, true, false, ""},
{"version long", []string{"gt", "--version"}, "", "", "", "", false, true, false, ""},
{"version word", []string{"gt", "version"}, "", "", "", "", false, true, false, ""},
{"worktree name only", []string{"gt", "feature-x"}, "feature-x", "", "", "", false, false, false, ""},
{"worktree with source", []string{"gt", "feature-x", "main"}, "feature-x", "main", "", "", false, false, false, ""},
{"execute short", []string{"gt", "feature-x", "-x", "npm install"}, "feature-x", "", "npm install", "", false, false, false, ""},
{"execute long", []string{"gt", "feature-x", "--execute", "npm install"}, "feature-x", "", "npm install", "", false, false, false, ""},
{"execute equals short", []string{"gt", "feature-x", "-x=code ."}, "feature-x", "", "code .", "", false, false, false, ""},
{"execute equals long", []string{"gt", "feature-x", "--execute=code ."}, "feature-x", "", "code .", "", false, false, false, ""},
{"completion bash", []string{"gt", "completion", "bash"}, "", "", "", "bash", false, false, false, ""},
{"completion zsh", []string{"gt", "completion", "zsh"}, "", "", "", "zsh", false, false, false, ""},
{"completion fish", []string{"gt", "completion", "fish"}, "", "", "", "fish", false, false, false, ""},
{"completion default", []string{"gt", "completion"}, "", "", "", "bash", false, false, false, ""},
{"merge mode", []string{"gt", "--merge", "feature-x"}, "feature-x", "", "", "", false, false, true, "ff-only"},
{"merge with squash", []string{"gt", "--merge", "feature-x", "--squash"}, "feature-x", "", "", "", false, false, true, "squash"},
{"merge with ff-only", []string{"gt", "--merge", "feature-x", "--ff-only"}, "feature-x", "", "", "", false, false, true, "ff-only"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save and restore os.Args
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = tt.args
worktree, source, execute, completion, help, version, merge, mergeStrat := parseArgs()
if worktree != tt.wantWorktree {
t.Errorf("worktree = %q, want %q", worktree, tt.wantWorktree)
}
if source != tt.wantSource {
t.Errorf("source = %q, want %q", source, tt.wantSource)
}
if execute != tt.wantExecute {
t.Errorf("execute = %q, want %q", execute, tt.wantExecute)
}
if completion != tt.wantCompletion {
t.Errorf("completion = %q, want %q", completion, tt.wantCompletion)
}
if help != tt.wantHelp {
t.Errorf("help = %v, want %v", help, tt.wantHelp)
}
if version != tt.wantVersion {
t.Errorf("version = %v, want %v", version, tt.wantVersion)
}
if merge != tt.wantMerge {
t.Errorf("merge = %v, want %v", merge, tt.wantMerge)
}
if mergeStrat != tt.wantMergeStrat {
t.Errorf("mergeStrat = %q, want %q", mergeStrat, tt.wantMergeStrat)
}
})
}
}