Skip to content

Commit 03906b7

Browse files
authored
Merge pull request #222 from roots/fix-autocomplete
Fix CLI autocompletion
2 parents 88b63bd + 53dd59d commit 03906b7

File tree

2 files changed

+112
-78
lines changed

2 files changed

+112
-78
lines changed

trellis/complete.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ func (t *Trellis) PredictSite() complete.PredictFunc {
1919
}
2020

2121
switch len(args.Completed) {
22-
case 1:
22+
case 0:
2323
return t.EnvironmentNames()
24-
case 2:
24+
case 1:
2525
return t.SiteNamesFromEnvironment(args.LastCompleted)
2626
default:
2727
return []string{}
@@ -36,7 +36,7 @@ func (t *Trellis) PredictEnvironment() complete.PredictFunc {
3636
}
3737

3838
switch len(args.Completed) {
39-
case 1:
39+
case 0:
4040
return t.EnvironmentNames()
4141
default:
4242
return []string{}

trellis/complete_test.go

Lines changed: 109 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,26 @@
11
package trellis
22

33
import (
4+
"bytes"
5+
"io"
6+
"os"
7+
"reflect"
8+
"sort"
49
"strings"
510
"testing"
611

12+
"github.com/mitchellh/cli"
713
"github.com/posener/complete"
814
)
915

10-
func TestPredictEnvironment(t *testing.T) {
16+
// Tests based on
17+
// https://github.com/mitchellh/cli/blob/5454ffe87bc5c6d8b6b21c825617755e18a07828/cli_test.go#L1125-L1225
18+
19+
// envComplete is the env var that the complete library sets to specify
20+
// it should be calculating an auto-completion.
21+
const envComplete = "COMP_LINE"
22+
23+
func TestCompletionFunctions(t *testing.T) {
1124
project := &Project{}
1225
trellis := NewTrellis(project)
1326

@@ -18,93 +31,114 @@ func TestPredictEnvironment(t *testing.T) {
1831
}
1932

2033
cases := []struct {
21-
name string
22-
completed []string
23-
want []string
34+
Predictor complete.Predictor
35+
Completed []string
36+
Last string
37+
Expected []string
2438
}{
25-
{
26-
name: "No args completed",
27-
completed: []string{},
28-
want: []string{},
29-
},
30-
{
31-
name: "Command supplied",
32-
completed: []string{"command"},
33-
want: []string{"development", "production", "valet-link"},
34-
},
35-
{
36-
name: "Command and env supplied",
37-
completed: []string{"command", "development"},
38-
want: []string{},
39-
},
39+
{trellis.AutocompleteEnvironment(), []string{"deploy"}, "", []string{"development", "valet-link", "production"}},
40+
{trellis.AutocompleteEnvironment(), []string{"deploy"}, "d", []string{"development"}},
41+
{trellis.AutocompleteEnvironment(), []string{"deploy", "production"}, "", nil},
42+
{trellis.AutocompleteSite(), []string{"deploy"}, "", []string{"development", "valet-link", "production"}},
43+
{trellis.AutocompleteSite(), []string{"deploy"}, "d", []string{"development"}},
44+
{trellis.AutocompleteSite(), []string{"deploy", "production"}, "", []string{"example.com"}},
4045
}
4146

4247
for _, tc := range cases {
43-
matches := trellis.PredictEnvironment().Predict(
44-
complete.Args{Completed: tc.completed},
45-
)
46-
47-
got := strings.Join(matches, ",")
48-
want := strings.Join(tc.want, ",")
49-
50-
if got != want {
51-
t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want)
52-
}
48+
t.Run(tc.Last, func(t *testing.T) {
49+
command := new(cli.MockCommandAutocomplete)
50+
command.AutocompleteArgsValue = tc.Predictor
51+
52+
cli := &cli.CLI{
53+
Commands: map[string]cli.CommandFactory{
54+
"deploy": func() (cli.Command, error) { return command, nil },
55+
},
56+
Autocomplete: true,
57+
}
58+
59+
// Setup the autocomplete line
60+
var input bytes.Buffer
61+
input.WriteString("cli ")
62+
if len(tc.Completed) > 0 {
63+
input.WriteString(strings.Join(tc.Completed, " "))
64+
input.WriteString(" ")
65+
}
66+
input.WriteString(tc.Last)
67+
defer testAutocomplete(t, input.String())()
68+
69+
// Setup the output so that we can read it. We don't need to
70+
// reset os.Stdout because testAutocomplete will do that for us.
71+
r, w, err := os.Pipe()
72+
if err != nil {
73+
t.Fatalf("err: %s", err)
74+
}
75+
defer r.Close() // Only defer reader since writer is closed below
76+
os.Stdout = w
77+
78+
// Run
79+
exitCode, err := cli.Run()
80+
w.Close()
81+
if err != nil {
82+
t.Fatalf("err: %s", err)
83+
}
84+
85+
if exitCode != 0 {
86+
t.Fatalf("bad: %d", exitCode)
87+
}
88+
89+
// Copy the output and get the autocompletions. We trim the last
90+
// element if we have one since we usually output a final newline
91+
// which results in a blank.
92+
var outBuf bytes.Buffer
93+
io.Copy(&outBuf, r)
94+
actual := strings.Split(outBuf.String(), "\n")
95+
if len(actual) > 0 {
96+
actual = actual[:len(actual)-1]
97+
}
98+
if len(actual) == 0 {
99+
// If we have no elements left, make the value nil since
100+
// this is what we use in tests.
101+
actual = nil
102+
}
103+
104+
sort.Strings(actual)
105+
sort.Strings(tc.Expected)
106+
107+
if !reflect.DeepEqual(actual, tc.Expected) {
108+
t.Fatalf("bad:\n\n%#v\n\n%#v", actual, tc.Expected)
109+
}
110+
})
53111
}
54112
}
55113

56-
func TestPredictSite(t *testing.T) {
57-
project := &Project{}
58-
trellis := NewTrellis(project)
114+
// testAutocomplete sets up the environment to behave like a <tab> was
115+
// pressed in a shell to autocomplete a command.
116+
func testAutocomplete(t *testing.T, input string) func() {
117+
// This env var is used to trigger autocomplete
118+
os.Setenv(envComplete, input)
59119

60-
defer TestChdir(t, "testdata/trellis")()
120+
// Change stdout/stderr since the autocompleter writes directly to them.
121+
oldStdout := os.Stdout
122+
oldStderr := os.Stderr
61123

62-
if err := trellis.LoadProject(); err != nil {
63-
t.Fatalf(err.Error())
124+
r, w, err := os.Pipe()
125+
if err != nil {
126+
t.Fatalf("err: %s", err)
64127
}
65128

66-
cases := []struct {
67-
name string
68-
completed []string
69-
lastCompleted string
70-
want []string
71-
}{
72-
{
73-
name: "No args completed",
74-
completed: []string{},
75-
lastCompleted: "",
76-
want: []string{},
77-
},
78-
{
79-
name: "Command supplied",
80-
completed: []string{"command"},
81-
lastCompleted: "command",
82-
want: []string{"development", "production", "valet-link"},
83-
},
84-
{
85-
name: "Command and env supplied",
86-
completed: []string{"command", "development"},
87-
lastCompleted: "development",
88-
want: []string{"example.com"},
89-
},
90-
{
91-
name: "Command, env, and site supplied",
92-
completed: []string{"command", "development", "example.com"},
93-
lastCompleted: "example.com",
94-
want: []string{},
95-
},
96-
}
129+
os.Stdout = w
130+
os.Stderr = w
97131

98-
for _, tc := range cases {
99-
matches := trellis.PredictSite().Predict(
100-
complete.Args{Completed: tc.completed, LastCompleted: tc.lastCompleted},
101-
)
132+
return func() {
133+
// Reset our env
134+
os.Unsetenv(envComplete)
102135

103-
got := strings.Join(matches, ",")
104-
want := strings.Join(tc.want, ",")
136+
// Reset stdout, stderr
137+
os.Stdout = oldStdout
138+
os.Stderr = oldStderr
105139

106-
if got != want {
107-
t.Errorf("failed %s\ngot = %s\nwant: %s", t.Name(), got, want)
108-
}
140+
// Close our pipe
141+
r.Close()
142+
w.Close()
109143
}
110144
}

0 commit comments

Comments
 (0)