Skip to content

Commit 2911ab1

Browse files
authored
[test] Improve test coverage above 70% (#24)
test(cmd): improve test coverage
1 parent 7a514d0 commit 2911ab1

16 files changed

Lines changed: 1280 additions & 29 deletions

cmd/banner_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cmd
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestPrintBanner(t *testing.T) {
9+
cmd, buf := newTestCmd()
10+
printBanner(cmd)
11+
12+
out := buf.String()
13+
// The ASCII art contains these patterns
14+
if !strings.Contains(out, "(_)") {
15+
t.Errorf("banner output does not contain ASCII art: %q", out)
16+
}
17+
if !strings.Contains(out, "v"+Version()) {
18+
t.Errorf("banner output does not contain version %q: %q", "v"+Version(), out)
19+
}
20+
}

cmd/clean_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"testing"
7+
)
8+
9+
// mergedWorktreeRunner returns a mockRunner that supports MergedBranches and ListWorktrees.
10+
func mergedWorktreeRunner(mergedOut, worktreeOut string) *mockRunner {
11+
return &mockRunner{
12+
run: func(args ...string) (string, error) {
13+
if len(args) >= 1 && args[0] == cmdBranch {
14+
return mergedOut, nil
15+
}
16+
if len(args) >= 1 && args[0] == cmdWorktreeTest {
17+
return worktreeOut, nil
18+
}
19+
return "", nil
20+
},
21+
runInDir: noopRunInDir,
22+
}
23+
}
24+
25+
func TestFindMergedCandidatesFound(t *testing.T) {
26+
worktreeOut := strings.Join([]string{
27+
"worktree /repo",
28+
"HEAD abc123",
29+
"branch refs/heads/main",
30+
"",
31+
"worktree " + pathWtDone,
32+
"HEAD def456",
33+
"branch refs/heads/" + branchDone,
34+
"",
35+
"worktree /wt/feature-active",
36+
"HEAD ghi789",
37+
"branch refs/heads/feature/active",
38+
"",
39+
}, "\n")
40+
41+
r := mergedWorktreeRunner(" "+branchDone+"\n bugfix/old", worktreeOut)
42+
candidates, err := findMergedCandidates(r, branchMain)
43+
if err != nil {
44+
t.Fatalf("findMergedCandidates: %v", err)
45+
}
46+
if len(candidates) != 1 {
47+
t.Fatalf("got %d candidates, want 1", len(candidates))
48+
}
49+
if candidates[0].branch != branchDone {
50+
t.Errorf("branch = %q, want %q", candidates[0].branch, branchDone)
51+
}
52+
}
53+
54+
func TestFindMergedCandidatesNone(t *testing.T) {
55+
r := mergedWorktreeRunner("", "worktree /repo\nHEAD abc\nbranch refs/heads/main\n")
56+
candidates, err := findMergedCandidates(r, branchMain)
57+
if err != nil {
58+
t.Fatalf("findMergedCandidates: %v", err)
59+
}
60+
if len(candidates) != 0 {
61+
t.Errorf("expected 0 candidates, got %d", len(candidates))
62+
}
63+
}
64+
65+
func TestFindMergedCandidatesError(t *testing.T) {
66+
r := &mockRunner{
67+
run: func(args ...string) (string, error) {
68+
if len(args) >= 1 && args[0] == cmdBranch {
69+
return "", errGitFailed
70+
}
71+
return "", nil
72+
},
73+
runInDir: noopRunInDir,
74+
}
75+
76+
_, err := findMergedCandidates(r, branchMain)
77+
if err == nil {
78+
t.Fatal(errExpected)
79+
}
80+
}
81+
82+
func TestPrintMergedCandidates(t *testing.T) {
83+
cmd, buf := newTestCmd()
84+
candidates := []mergedCandidate{
85+
{path: pathWtDone, branch: branchDone},
86+
{path: "/wt/bugfix-old", branch: "bugfix/old"},
87+
}
88+
printMergedCandidates(cmd, candidates)
89+
90+
out := buf.String()
91+
if !strings.Contains(out, "Merged worktrees:") {
92+
t.Errorf("output missing header: %q", out)
93+
}
94+
if !strings.Contains(out, "done") {
95+
t.Errorf("output missing task 'done': %q", out)
96+
}
97+
if !strings.Contains(out, "old") {
98+
t.Errorf("output missing task 'old': %q", out)
99+
}
100+
}
101+
102+
func testMergedCandidate() []mergedCandidate {
103+
return []mergedCandidate{{path: pathWtDone, branch: branchDone}}
104+
}
105+
106+
func TestRemoveMergedWorktreesAllSucceed(t *testing.T) {
107+
cmd, _ := newTestCmd()
108+
r := &mockRunner{
109+
run: func(_ ...string) (string, error) { return "", nil },
110+
runInDir: noopRunInDir,
111+
}
112+
113+
removed := removeMergedWorktrees(cmd, r, testMergedCandidate())
114+
if removed != 1 {
115+
t.Errorf("removed = %d, want 1", removed)
116+
}
117+
}
118+
119+
func TestRemoveMergedWorktreesRemoveFails(t *testing.T) {
120+
cmd, buf := newTestCmd()
121+
r := &mockRunner{
122+
run: func(args ...string) (string, error) {
123+
if len(args) >= 2 && args[0] == cmdWorktreeTest && args[1] == "remove" {
124+
return "", errors.New("locked")
125+
}
126+
return "", nil
127+
},
128+
runInDir: noopRunInDir,
129+
}
130+
131+
removed := removeMergedWorktrees(cmd, r, testMergedCandidate())
132+
if removed != 0 {
133+
t.Errorf("removed = %d, want 0", removed)
134+
}
135+
if !strings.Contains(buf.String(), "Failed to remove") {
136+
t.Errorf("output = %q, want failure message", buf.String())
137+
}
138+
}
139+
140+
func TestRemoveMergedWorktreesDeleteBranchFails(t *testing.T) {
141+
cmd, buf := newTestCmd()
142+
r := &mockRunner{
143+
run: func(args ...string) (string, error) {
144+
if len(args) >= 1 && args[0] == cmdBranch {
145+
return "", errors.New("branch not found")
146+
}
147+
return "", nil
148+
},
149+
runInDir: noopRunInDir,
150+
}
151+
152+
removed := removeMergedWorktrees(cmd, r, testMergedCandidate())
153+
if removed != 0 {
154+
t.Errorf("removed = %d, want 0 (branch delete failed)", removed)
155+
}
156+
if !strings.Contains(buf.String(), "failed to delete branch") {
157+
t.Errorf("output = %q, want branch delete failure message", buf.String())
158+
}
159+
}
160+
161+
func TestConfirmRemoval(t *testing.T) {
162+
tests := []struct {
163+
name string
164+
input string
165+
want bool
166+
}{
167+
{"yes_short", "y\n", true},
168+
{"yes_full", "yes\n", true},
169+
{"no", "n\n", false},
170+
{"empty", "\n", false},
171+
}
172+
173+
for _, tt := range tests {
174+
t.Run(tt.name, func(t *testing.T) {
175+
cmd, _ := newTestCmd()
176+
cmd.SetIn(strings.NewReader(tt.input))
177+
got := confirmRemoval(cmd, 2)
178+
if got != tt.want {
179+
t.Errorf("confirmRemoval(%q) = %v, want %v", tt.input, got, tt.want)
180+
}
181+
})
182+
}
183+
}

cmd/colors_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cmd
2+
3+
import (
4+
"testing"
5+
6+
"github.com/lugassawan/rimba/internal/resolver"
7+
"github.com/lugassawan/rimba/internal/termcolor"
8+
)
9+
10+
func TestTypeColor(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
input string
14+
want termcolor.Color
15+
}{
16+
{"feature", "feature", termcolor.Cyan},
17+
{"bugfix", "bugfix", termcolor.Yellow},
18+
{"hotfix", "hotfix", termcolor.Red},
19+
{"docs", "docs", termcolor.Blue},
20+
{"test", "test", termcolor.Magenta},
21+
{"chore", "chore", termcolor.Gray},
22+
{"unknown", "other", termcolor.Color("")},
23+
}
24+
25+
for _, tt := range tests {
26+
t.Run(tt.name, func(t *testing.T) {
27+
got := typeColor(tt.input)
28+
if got != tt.want {
29+
t.Errorf("typeColor(%q) = %q, want %q", tt.input, got, tt.want)
30+
}
31+
})
32+
}
33+
}
34+
35+
func TestColorStatus(t *testing.T) {
36+
p := termcolor.NewPainter(true) // no-color mode for predictable output
37+
38+
tests := []struct {
39+
name string
40+
status resolver.WorktreeStatus
41+
want string
42+
}{
43+
{
44+
name: "clean",
45+
status: resolver.WorktreeStatus{},
46+
want: "✓",
47+
},
48+
{
49+
name: "dirty only",
50+
status: resolver.WorktreeStatus{Dirty: true},
51+
want: "[dirty]",
52+
},
53+
{
54+
name: "ahead only",
55+
status: resolver.WorktreeStatus{Ahead: 3},
56+
want: "↑3",
57+
},
58+
{
59+
name: "behind only",
60+
status: resolver.WorktreeStatus{Behind: 2},
61+
want: "↓2",
62+
},
63+
{
64+
name: "dirty and ahead and behind",
65+
status: resolver.WorktreeStatus{Dirty: true, Ahead: 2, Behind: 1},
66+
want: "[dirty] ↑2 ↓1",
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
got := colorStatus(p, tt.status)
73+
if got != tt.want {
74+
t.Errorf("colorStatus(%+v) = %q, want %q", tt.status, got, tt.want)
75+
}
76+
})
77+
}
78+
}

cmd/completions_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package cmd
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestCompleteWorktreeTasks(t *testing.T) {
9+
porcelain := strings.Join([]string{
10+
"worktree /repo",
11+
"HEAD abc123",
12+
"branch refs/heads/main",
13+
"",
14+
"worktree /wt/feature-login",
15+
"HEAD def456",
16+
"branch refs/heads/" + branchFeature,
17+
"",
18+
"worktree /wt/bugfix-typo",
19+
"HEAD ghi789",
20+
"branch refs/heads/bugfix/typo",
21+
"",
22+
}, "\n")
23+
24+
r := &mockRunner{
25+
run: func(_ ...string) (string, error) { return porcelain, nil },
26+
runInDir: noopRunInDir,
27+
}
28+
29+
restore := overrideNewRunner(r)
30+
defer restore()
31+
32+
cmd, _ := newTestCmd()
33+
34+
t.Run("complete all", func(t *testing.T) {
35+
tasks := completeWorktreeTasks(cmd, "")
36+
if len(tasks) < 2 {
37+
t.Fatalf("expected at least 2 tasks, got %d: %v", len(tasks), tasks)
38+
}
39+
found := map[string]bool{}
40+
for _, task := range tasks {
41+
found[task] = true
42+
}
43+
if !found["login"] {
44+
t.Error("expected 'login' in completions")
45+
}
46+
if !found["typo"] {
47+
t.Error("expected 'typo' in completions")
48+
}
49+
})
50+
51+
t.Run("complete with prefix filter", func(t *testing.T) {
52+
tasks := completeWorktreeTasks(cmd, "log")
53+
if len(tasks) != 1 {
54+
t.Fatalf("expected 1 task, got %d: %v", len(tasks), tasks)
55+
}
56+
if tasks[0] != "login" {
57+
t.Errorf("task = %q, want %q", tasks[0], "login")
58+
}
59+
})
60+
}
61+
62+
func TestCompleteBranchNames(t *testing.T) {
63+
r := &mockRunner{
64+
run: func(_ ...string) (string, error) { return "main\nfeature/login\nbugfix/typo\n", nil },
65+
runInDir: noopRunInDir,
66+
}
67+
68+
restore := overrideNewRunner(r)
69+
defer restore()
70+
71+
cmd, _ := newTestCmd()
72+
73+
t.Run("complete all", func(t *testing.T) {
74+
branches := completeBranchNames(cmd, "")
75+
if len(branches) != 3 {
76+
t.Fatalf("expected 3 branches, got %d: %v", len(branches), branches)
77+
}
78+
})
79+
80+
t.Run("complete with prefix filter", func(t *testing.T) {
81+
branches := completeBranchNames(cmd, "feature")
82+
if len(branches) != 1 {
83+
t.Fatalf("expected 1 branch, got %d: %v", len(branches), branches)
84+
}
85+
if branches[0] != branchFeature {
86+
t.Errorf("branch = %q, want %q", branches[0], branchFeature)
87+
}
88+
})
89+
}
90+
91+
func TestCompleteWorktreeTasksError(t *testing.T) {
92+
r := &mockRunner{
93+
run: func(_ ...string) (string, error) { return "", errGitFailed },
94+
runInDir: noopRunInDir,
95+
}
96+
97+
restore := overrideNewRunner(r)
98+
defer restore()
99+
100+
cmd, _ := newTestCmd()
101+
tasks := completeWorktreeTasks(cmd, "")
102+
if tasks != nil {
103+
t.Errorf("expected nil tasks on error, got %v", tasks)
104+
}
105+
}

0 commit comments

Comments
 (0)