command_test.go 7.96 KB
Newer Older
1
package cmds
2

3
import (
4
	"context"
5
	"errors"
keks's avatar
keks committed
6
	"fmt"
7 8
	"io"
	"testing"
9
	"time"
10 11
)

keks's avatar
keks committed
12
// NOTE: helpers nopCloser, testEmitter, noop and writeCloser are defined in helpers_test.go
13

Jan Winkelmann's avatar
Jan Winkelmann committed
14
// TestOptionValidation tests whether option type validation works
15
func TestOptionValidation(t *testing.T) {
16
	cmd := &Command{
Steven Allen's avatar
Steven Allen committed
17 18 19
		Options: []Option{
			IntOption("b", "beep", "enables beeper"),
			StringOption("B", "boop", "password for booper"),
20
			StringsOption("S", "shoop", "what to shoop"),
Matt Bell's avatar
Matt Bell committed
21
		},
Matt Bell's avatar
Matt Bell committed
22
		Run: noop,
Matt Bell's avatar
Matt Bell committed
23 24
	}

keks's avatar
keks committed
25 26 27
	type testcase struct {
		opts            map[string]interface{}
		NewRequestError string
Matt Bell's avatar
Matt Bell committed
28 29
	}

keks's avatar
keks committed
30 31 32 33 34 35 36 37 38 39 40 41
	mkTest := func(tc testcase) func(*testing.T) {
		return func(t *testing.T) {
			re := newTestEmitter(t)
			req, err := NewRequest(context.Background(), nil, tc.opts, nil, nil, cmd)
			if tc.NewRequestError == "" {
				if err != nil {
					t.Errorf("unexpected error %q", err)
				}

				cmd.Call(req, re, nil)
			} else {
				if err == nil {
Hector Sanjuan's avatar
Hector Sanjuan committed
42
					t.Errorf("should have failed with error %q", tc.NewRequestError)
keks's avatar
keks committed
43 44 45 46 47
				} else if err.Error() != tc.NewRequestError {
					t.Errorf("expected error %q, got %q", tc.NewRequestError, err)
				}
			}
		}
48
	}
Matt Bell's avatar
Matt Bell committed
49

keks's avatar
keks committed
50 51 52
	tcs := []testcase{
		{
			opts:            map[string]interface{}{"boop": true},
Hector Sanjuan's avatar
Hector Sanjuan committed
53
			NewRequestError: `option "boop" should be type "string", but got type "bool"`,
keks's avatar
keks committed
54 55 56 57 58 59 60
		},
		{opts: map[string]interface{}{"beep": 5}},
		{opts: map[string]interface{}{"beep": 5, "boop": "test"}},
		{opts: map[string]interface{}{"b": 5, "B": "test"}},
		{opts: map[string]interface{}{"foo": 5}},
		{opts: map[string]interface{}{EncLong: "json"}},
		{opts: map[string]interface{}{"beep": "100"}},
61 62 63
		{opts: map[string]interface{}{"S": [2]string{"a", "b"}}},
		{
			opts:            map[string]interface{}{"S": true},
Hector Sanjuan's avatar
Hector Sanjuan committed
64
			NewRequestError: `option "S" should be type "array", but got type "bool"`},
keks's avatar
keks committed
65 66
		{
			opts:            map[string]interface{}{"beep": ":)"},
Hector Sanjuan's avatar
Hector Sanjuan committed
67
			NewRequestError: `could not convert value ":)" to type "int" (for option "-beep")`,
keks's avatar
keks committed
68
		},
69
	}
70

keks's avatar
keks committed
71 72
	for i, tc := range tcs {
		t.Run(fmt.Sprint(i), mkTest(tc))
73
	}
74
}
75 76

func TestRegistration(t *testing.T) {
Matt Bell's avatar
Matt Bell committed
77
	cmdA := &Command{
Steven Allen's avatar
Steven Allen committed
78 79
		Options: []Option{
			IntOption("beep", "number of beeps"),
Matt Bell's avatar
Matt Bell committed
80
		},
Matt Bell's avatar
Matt Bell committed
81 82
		Run: noop,
	}
Matt Bell's avatar
Matt Bell committed
83

Matt Bell's avatar
Matt Bell committed
84
	cmdB := &Command{
Steven Allen's avatar
Steven Allen committed
85 86
		Options: []Option{
			IntOption("beep", "number of beeps"),
Matt Bell's avatar
Matt Bell committed
87
		},
Matt Bell's avatar
Matt Bell committed
88 89 90
		Run: noop,
		Subcommands: map[string]*Command{
			"a": cmdA,
Matt Bell's avatar
Matt Bell committed
91 92 93
		},
	}

94 95 96
	path := []string{"a"}
	_, err := cmdB.GetOptions(path)
	if err == nil {
Matt Bell's avatar
Matt Bell committed
97 98
		t.Error("Should have failed (option name collision)")
	}
99
}
100 101 102

func TestResolving(t *testing.T) {
	cmdC := &Command{}
Matt Bell's avatar
Matt Bell committed
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
	cmdB := &Command{
		Subcommands: map[string]*Command{
			"c": cmdC,
		},
	}
	cmdB2 := &Command{}
	cmdA := &Command{
		Subcommands: map[string]*Command{
			"b": cmdB,
			"B": cmdB2,
		},
	}
	cmd := &Command{
		Subcommands: map[string]*Command{
			"a": cmdA,
		},
	}
120

Matt Bell's avatar
Matt Bell committed
121
	cmds, err := cmd.Resolve([]string{"a", "b", "c"})
122 123 124 125 126 127
	if err != nil {
		t.Error(err)
	}
	if len(cmds) != 4 || cmds[0] != cmd || cmds[1] != cmdA || cmds[2] != cmdB || cmds[3] != cmdC {
		t.Error("Returned command path is different than expected", cmds)
	}
Matt Bell's avatar
Matt Bell committed
128
}
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147

func TestWalking(t *testing.T) {
	cmdA := &Command{
		Subcommands: map[string]*Command{
			"b": &Command{},
			"B": &Command{},
		},
	}
	i := 0
	cmdA.Walk(func(c *Command) {
		i = i + 1
	})
	if i != 3 {
		t.Error("Command tree walk didn't work, expected 3 got:", i)
	}
}

func TestHelpProcessing(t *testing.T) {
	cmdB := &Command{
Steven Allen's avatar
Steven Allen committed
148
		Helptext: HelpText{
149 150 151 152
			ShortDescription: "This is other short",
		},
	}
	cmdA := &Command{
Steven Allen's avatar
Steven Allen committed
153
		Helptext: HelpText{
154 155 156 157 158 159 160 161 162 163 164 165 166 167
			ShortDescription: "This is short",
		},
		Subcommands: map[string]*Command{
			"a": cmdB,
		},
	}
	cmdA.ProcessHelp()
	if len(cmdA.Helptext.LongDescription) == 0 {
		t.Error("LongDescription was not set on basis of ShortDescription")
	}
	if len(cmdB.Helptext.LongDescription) == 0 {
		t.Error("LongDescription was not set on basis of ShortDescription")
	}
}
168 169 170

type postRunTestCase struct {
	length      uint64
Steven Allen's avatar
Steven Allen committed
171
	err         *Error
172
	emit        []interface{}
keks's avatar
keks committed
173
	postRun     func(Response, ResponseEmitter) error
174 175 176 177
	next        []interface{}
	finalLength uint64
}

Jan Winkelmann's avatar
Jan Winkelmann committed
178
// TestPostRun tests whether commands with PostRun return the intended result
179
func TestPostRun(t *testing.T) {
180 181 182
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

183 184 185 186 187 188 189
	var testcases = []postRunTestCase{
		postRunTestCase{
			length:      3,
			err:         nil,
			emit:        []interface{}{7},
			finalLength: 4,
			next:        []interface{}{14},
keks's avatar
keks committed
190 191 192 193 194 195
			postRun: func(res Response, re ResponseEmitter) error {
				l := res.Length()
				re.SetLength(l + 1)

				for {
					v, err := res.Next()
keks's avatar
keks committed
196
					t.Log("PostRun: Next returned", v, err)
keks's avatar
keks committed
197 198
					if err != nil {
						return err
199
					}
200

keks's avatar
keks committed
201 202 203 204 205 206 207
					i := v.(int)

					err = re.Emit(2 * i)
					if err != nil {
						return err
					}
				}
208 209 210 211 212 213
			},
		},
	}

	for _, tc := range testcases {
		cmd := &Command{
214
			Run: func(req *Request, re ResponseEmitter, env Environment) error {
215 216
				re.SetLength(tc.length)

217 218
				for _, v := range tc.emit {
					err := re.Emit(v)
219 220 221
					if err != nil {
						t.Fatal(err)
					}
222
				}
223
				return nil
224
			},
Jan Winkelmann's avatar
Jan Winkelmann committed
225
			PostRun: PostRunMap{
226 227 228 229
				CLI: tc.postRun,
			},
		}

230
		req, err := NewRequest(ctx, nil, map[string]interface{}{
231 232
			EncLong: CLI,
		}, nil, nil, cmd)
233 234 235
		if err != nil {
			t.Fatal(err)
		}
236

237
		opts := req.Options
238 239 240 241
		if opts == nil {
			t.Fatal("req.Options() is nil")
		}

242
		encTypeIface := opts[EncLong]
243
		if encTypeIface == nil {
244
			t.Fatal("req.Options()[EncLong] is nil")
245 246 247 248 249 250 251 252 253 254 255
		}

		encType := EncodingType(encTypeIface.(string))
		if encType == "" {
			t.Fatal("no encoding type")
		}

		if encType != CLI {
			t.Fatal("wrong encoding type")
		}

keks's avatar
keks committed
256 257 258 259 260
		postre, res := NewChanResponsePair(req)
		re, postres := NewChanResponsePair(req)

		go func() {
			err := cmd.PostRun[PostRunType(encType)](postres, postre)
keks's avatar
keks committed
261
			err = postre.CloseWithError(err)
keks's avatar
keks committed
262
			if err != nil {
keks's avatar
keks committed
263
				t.Error("error closing after PostRun: ", err)
keks's avatar
keks committed
264 265
			}
		}()
266

267
		cmd.Call(req, re, nil)
268 269 270 271 272 273 274 275 276 277 278

		l := res.Length()
		if l != tc.finalLength {
			t.Fatal("wrong final length")
		}

		for _, x := range tc.next {
			ch := make(chan interface{})

			go func() {
				v, err := res.Next()
keks's avatar
keks committed
279
				t.Log("next returned", v, err)
280 281
				if err != nil {
					close(ch)
Hector Sanjuan's avatar
Hector Sanjuan committed
282 283
					t.Error(err)
					return
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
				}

				ch <- v
			}()

			select {
			case v, ok := <-ch:
				if !ok {
					t.Fatal("error checking all next values - channel closed")
				}
				if x != v {
					t.Fatalf("final check of emitted values failed. got %v but expected %v", v, x)
				}
			case <-time.After(50 * time.Millisecond):
				t.Fatal("too few values in next")
			}
		}
Jan Winkelmann's avatar
Jan Winkelmann committed
301 302 303 304 305

		_, err = res.Next()
		if err != io.EOF {
			t.Fatal("expected EOF, got", err)
		}
306 307
	}
}
308 309 310 311 312

func TestCancel(t *testing.T) {
	wait := make(chan struct{})
	ctx, cancel := context.WithCancel(context.Background())

keks's avatar
keks committed
313
	req, err := NewRequest(ctx, nil, nil, nil, nil, &Command{})
314 315 316 317 318 319 320 321 322
	if err != nil {
		t.Fatal(err)
	}

	re, res := NewChanResponsePair(req)

	go func() {
		err := re.Emit("abc")
		if err != context.Canceled {
323 324 325
			t.Errorf("re:  expected context.Canceled but got %v", err)
		} else {
			t.Log("re.Emit err:", err)
326 327 328 329 330 331 332 333 334
		}
		re.Close()
		close(wait)
	}()

	cancel()

	_, err = res.Next()
	if err != context.Canceled {
335 336 337
		t.Errorf("res: expected context.Canceled but got %v", err)
	} else {
		t.Log("res.Emit err:", err)
338 339 340
	}
	<-wait
}
341 342 343 344 345 346 347 348 349

type testEmitterWithError struct{ errorCount int }

func (s *testEmitterWithError) Close() error {
	return nil
}

func (s *testEmitterWithError) SetLength(_ uint64) {}

keks's avatar
keks committed
350
func (s *testEmitterWithError) CloseWithError(err error) error {
351
	s.errorCount++
keks's avatar
keks committed
352
	return nil
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382
}

func (s *testEmitterWithError) Emit(value interface{}) error {
	return nil
}

func TestEmitterExpectError(t *testing.T) {
	cmd := &Command{
		Run: func(req *Request, re ResponseEmitter, env Environment) error {
			return errors.New("an error occurred")
		},
	}

	re := &testEmitterWithError{}
	req, err := NewRequest(context.Background(), nil, nil, nil, nil, cmd)

	if err != nil {
		t.Error("Should have passed")
	}

	cmd.Call(req, re, nil)

	switch re.errorCount {
	case 0:
		t.Errorf("expected SetError to be called")
	case 1:
	default:
		t.Errorf("expected SetError to be called once, but was called %d times", re.errorCount)
	}
}